@indigoai-us/hq-cli 5.311.4 → 5.312.1

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.
Files changed (96) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/assets/scaffold/core/scripts/lib/work-mesh-live-hook.sh +378 -0
  3. package/assets/scaffold/core/scripts/lib/work-mesh-live-rebind.sh +141 -0
  4. package/assets/scaffold/core/scripts/migrate-policy-triggers.sh +373 -0
  5. package/assets/scaffold/core/scripts/register-project.sh +262 -0
  6. package/assets/scaffold/core/scripts/work-mesh-live-rebind.sh +59 -0
  7. package/dist/command-catalog.generated.d.ts +160 -0
  8. package/dist/command-catalog.generated.js +200 -0
  9. package/dist/command-registration-plan.d.ts +6 -0
  10. package/dist/command-registration-plan.js +1 -0
  11. package/dist/commands/company-rename-flag.d.ts +4 -0
  12. package/dist/commands/company-rename-flag.js +7 -0
  13. package/dist/commands/company-rename.d.ts +4 -0
  14. package/dist/commands/company-rename.js +90 -0
  15. package/dist/commands/company.js +2 -0
  16. package/dist/commands/core-checkpoint-stop-gate-native.js +12 -0
  17. package/dist/commands/core-eval-trigger-native.d.ts +4 -0
  18. package/dist/commands/core-eval-trigger-native.js +12 -0
  19. package/dist/commands/core-migrate-policy-triggers-native.d.ts +21 -0
  20. package/dist/commands/core-migrate-policy-triggers-native.js +378 -0
  21. package/dist/commands/core-native-hook-helper-commands.d.ts +4 -0
  22. package/dist/commands/core-native-hook-helper-commands.js +56 -0
  23. package/dist/commands/core-native-hook-helpers-flag.d.ts +4 -0
  24. package/dist/commands/core-native-hook-helpers-flag.js +7 -0
  25. package/dist/commands/core-register-project-native.d.ts +30 -0
  26. package/dist/commands/core-register-project-native.js +347 -0
  27. package/dist/commands/core-work-mesh-live-rebind-native.d.ts +18 -0
  28. package/dist/commands/core-work-mesh-live-rebind-native.js +262 -0
  29. package/dist/commands/core.js +7 -0
  30. package/dist/commands/daemon.js +22 -0
  31. package/dist/commands/dm.js +22 -6
  32. package/dist/commands/files-browse.js +16 -4
  33. package/dist/commands/lanes-message.js +17 -2
  34. package/dist/commands/lanes-shared.d.ts +1 -0
  35. package/dist/commands/lanes.js +96 -20
  36. package/dist/commands/vault-retention-flag.d.ts +3 -0
  37. package/dist/commands/vault-retention-flag.js +9 -0
  38. package/dist/commands/vault-retention.d.ts +11 -0
  39. package/dist/commands/vault-retention.js +69 -0
  40. package/dist/fast-core.js +10 -3
  41. package/dist/fast-scaffold.d.ts +2 -0
  42. package/dist/fast-scaffold.js +11 -0
  43. package/dist/lib/agents/model-catalog.d.ts +3 -2
  44. package/dist/lib/company-skill-wrapper.d.ts +0 -8
  45. package/dist/lib/core-utils/codex-skill-bridge.d.ts +2 -1
  46. package/dist/lib/core-utils/sentry-report.js +21 -4
  47. package/dist/lib/daemon/catalog.d.ts +2 -0
  48. package/dist/lib/daemon/catalog.js +13 -1
  49. package/dist/lib/daemon/config.d.ts +2 -0
  50. package/dist/lib/daemon/config.js +4 -0
  51. package/dist/lib/daemon/run.d.ts +2 -0
  52. package/dist/lib/daemon/run.js +5 -0
  53. package/dist/lib/deploy-api.d.ts +0 -1
  54. package/dist/lib/deploy-api.js +1 -1
  55. package/dist/lib/doctor/checks/work-context.js +9 -1
  56. package/dist/lib/lanes/autopilot.d.ts +38 -1
  57. package/dist/lib/lanes/autopilot.js +353 -5
  58. package/dist/lib/lanes/flag-keys.d.ts +9 -1
  59. package/dist/lib/lanes/flag-keys.js +9 -1
  60. package/dist/lib/lanes/keep-alive.d.ts +2 -0
  61. package/dist/lib/lanes/keep-alive.js +2 -0
  62. package/dist/lib/lanes/reconcile.d.ts +5 -0
  63. package/dist/lib/lanes/reconcile.js +57 -4
  64. package/dist/lib/lanes/watcher-main.js +20 -1
  65. package/dist/lib/lanes/watcher.d.ts +9 -0
  66. package/dist/lib/lanes/watcher.js +47 -1
  67. package/dist/lib/mesh/client.js +23 -5
  68. package/dist/lib/mesh/denial-codes.d.ts +5 -0
  69. package/dist/lib/mesh/denial-codes.js +14 -0
  70. package/dist/lib/mesh/live/daemon/doctor.d.ts +2 -0
  71. package/dist/lib/mesh/live/daemon/doctor.js +11 -1
  72. package/dist/lib/report-run.d.ts +3 -2
  73. package/dist/lib/report-templates.d.ts +0 -1
  74. package/dist/lib/report-templates.js +0 -3
  75. package/dist/lib/scaffold-cli-floor.d.ts +0 -1
  76. package/dist/lib/scaffold-cli-floor.js +1 -1
  77. package/dist/lib/search-index/embed-lock.d.ts +2 -0
  78. package/dist/lib/search-index/embed-lock.js +10 -1
  79. package/dist/lib/work-context/contract.d.ts +3 -0
  80. package/dist/lib/work-context/contract.schema.json +3 -1
  81. package/dist/lib/work-context/outbox.d.ts +14 -5
  82. package/dist/lib/work-context/outbox.js +48 -5
  83. package/dist/lib/work-context/reconcile.js +26 -5
  84. package/dist/main.d.ts +20 -0
  85. package/dist/main.js +8 -1
  86. package/dist/run/discover-schemas.d.ts +2 -1
  87. package/dist/utils/core-eval-trigger.d.ts +4 -0
  88. package/dist/utils/core-eval-trigger.js +88 -0
  89. package/dist/utils/hq-roots.d.ts +0 -2
  90. package/dist/utils/hq-roots.js +1 -8
  91. package/dist/utils/incomplete-install-error.d.ts +7 -6
  92. package/dist/utils/large-file-guard.d.ts +0 -10
  93. package/dist/utils/large-file-guard.js +1 -1
  94. package/dist/utils/package-root-diagnostics.d.ts +1 -1
  95. package/dist/utils/self-update.d.ts +2 -1
  96. package/package.json +1 -1
@@ -474,6 +474,18 @@ async function runDaemonSyncNow(d, opts) {
474
474
  const result = { accepted: true, action: "restart-sync-runner", daemonRunning: running };
475
475
  d.out(opts.json ? JSON.stringify(result, null, 2) : running ? "Asked the daemon to start a fresh sync pass." : "Queued a fresh sync pass for the next daemon start.");
476
476
  }
477
+ async function runDaemonSyncInstantSyncSet(d, enabled, opts) {
478
+ const host = currentHost(d);
479
+ const config = readDaemonConfig(host.paths.configPath);
480
+ const changed = config.instantSyncEnabled !== enabled;
481
+ if (changed)
482
+ writeDaemonConfig(host.paths.configPath, { ...config, instantSyncEnabled: enabled });
483
+ const daemonRunning = runningDaemonPid(host.paths.lockPath, d.daemonAlive) !== null;
484
+ if (changed && daemonRunning)
485
+ queueDaemonSyncControl(d, { action: "restart-daemon" });
486
+ const result = { instantSyncEnabled: enabled, persisted: true, daemonRestartRequested: changed && daemonRunning };
487
+ d.out(opts.json ? JSON.stringify(result, null, 2) : `Instant Sync ${enabled ? "enabled" : "disabled"}${daemonRunning ? "; asked the daemon to restart" : "; saved for the next daemon start"}.`);
488
+ }
477
489
  async function runDaemonSyncPause(d, paused, opts) {
478
490
  const host = currentHost(d);
479
491
  const config = readDaemonConfig(host.paths.configPath);
@@ -772,6 +784,16 @@ export function registerDaemonCommand(program, over = {}) {
772
784
  .description("Start a fresh sync pass immediately")
773
785
  .option("--json", "Print machine-readable JSON")
774
786
  .action(wrap((d, opts) => runDaemonSyncNow(d, opts)));
787
+ const instantSync = sync.command("instant-sync").description("Control daemon Instant Sync");
788
+ instantSync
789
+ .command("set <state>")
790
+ .description("Set whether daemon sync uses Instant Sync")
791
+ .option("--json", "Print machine-readable JSON")
792
+ .action(wrap((d, state, opts) => {
793
+ if (state !== "on" && state !== "off")
794
+ throw new DaemonCommandError("Use on or off for Instant Sync.");
795
+ return runDaemonSyncInstantSyncSet(d, state === "on", opts);
796
+ }));
775
797
  sync
776
798
  .command("pause")
777
799
  .description("Pause daemon sync until resumed")
@@ -872,7 +872,12 @@ export function formatChannelMessage(m, nowMs, opts = {}) {
872
872
  const id = opts.showEventId ? (m.eventId ?? m.messageId) : undefined;
873
873
  const idPart = id ? ` ${chalk.dim(id)}` : "";
874
874
  const bodyIndent = `${indent} `;
875
- return `${indent}${who} ${when}${idPart}\n${bodyIndent}${firstLine(m.body)}${formatAttachments(m, bodyIndent)}`;
875
+ const body = firstLine(m.body);
876
+ const readableBody = body ||
877
+ (senderLabel(m).toLowerCase() === "hq"
878
+ ? "System notification (no text provided)."
879
+ : "(no message text)");
880
+ return `${indent}${who} ${when}${idPart}\n${bodyIndent}${readableBody}${formatAttachments(m, bodyIndent)}`;
876
881
  }
877
882
  /**
878
883
  * Read the caller's person/agent uid and email from a JWT we already hold.
@@ -1227,26 +1232,27 @@ async function runDmThread(identifier, opts) {
1227
1232
  * `hq channels`). Throws a user-facing message when nothing matches or a name is
1228
1233
  * ambiguous.
1229
1234
  */
1230
- async function resolveChannelId(token, target) {
1235
+ async function resolveChannel(token, target) {
1231
1236
  const raw = target.trim().replace(/^#/, "");
1232
1237
  if (!raw)
1233
1238
  throw new Error("A channel name or id is required.");
1234
1239
  const channels = await fetchChannels(token);
1235
1240
  const named = matchChannelsByName(channels, raw);
1236
1241
  if (named.length === 1)
1237
- return named[0].channelId;
1242
+ return named[0];
1238
1243
  if (named.length > 1) {
1239
1244
  throw new Error(`'${raw}' matches ${named.length} channels — pass the channel id instead (see \`hq channels\`).`);
1240
1245
  }
1241
1246
  const byId = channels.find((c) => c.channelId === raw);
1242
1247
  if (byId)
1243
- return byId.channelId;
1248
+ return byId;
1244
1249
  throw new Error(`No channel named or with id '${raw}' — run \`hq channels\` to see yours.`);
1245
1250
  }
1246
1251
  async function runDmChannel(target, opts) {
1247
1252
  try {
1248
1253
  const token = await ensureCognitoToken();
1249
- const channelId = await resolveChannelId(token, target);
1254
+ const channel = await resolveChannel(token, target);
1255
+ const channelId = channel.channelId;
1250
1256
  const query = {};
1251
1257
  if (opts.limit)
1252
1258
  query.limit = opts.limit;
@@ -1268,7 +1274,17 @@ async function runDmChannel(target, opts) {
1268
1274
  console.log(JSON.stringify(ordered, null, 2));
1269
1275
  }
1270
1276
  else if (ordered.length === 0) {
1271
- console.log(chalk.dim("No messages in this channel yet."));
1277
+ const channelType = channel.scope === "personal"
1278
+ ? "personal"
1279
+ : channel.scope === "group"
1280
+ ? "group"
1281
+ : "shared";
1282
+ const channelName = channel.name?.trim() || channel.slug?.trim() || channelId;
1283
+ const requestedTarget = target.trim().replace(/^#/, "");
1284
+ const sendTarget = requestedTarget === channelId
1285
+ ? channelId
1286
+ : channel.slug?.trim() || requestedTarget;
1287
+ console.log(chalk.dim(`No messages in ${channelType} channel "${channelName}" yet.\nSend a message with: hq dm ${sendTarget} "Hello"`));
1272
1288
  }
1273
1289
  else {
1274
1290
  console.log(chalk.green(`${ordered.length} message${ordered.length === 1 ? "" : "s"}:`));
@@ -63,6 +63,11 @@ export function parseCompanySlugFromPath(prefix) {
63
63
  }
64
64
  return parts[1];
65
65
  }
66
+ // Normalize personal paths before using them as S3 list prefixes.
67
+ function normalizePersonalBrowsePrefix(pathPrefix) {
68
+ const normalized = pathPrefix.replace(/^\/+/, "");
69
+ return /^[/.]+$/.test(normalized) ? "" : normalized;
70
+ }
66
71
  /**
67
72
  * Translate a CLI `companies/<slug>/…` path into the company-relative S3 key
68
73
  * the vault bucket actually stores. Company vault buckets are already
@@ -277,10 +282,17 @@ export async function runBrowse(input) {
277
282
  }
278
283
  // Company vault keys are company-relative (no `companies/<slug>/` prefix), so
279
284
  // translate the CLI's anchored prefix into the bucket-relative form before
280
- // listing. Personal-mode paths are already bucket-relative.
281
- const listPrefix = personalMode || slug === undefined
282
- ? pathPrefix
283
- : toBucketRelative(pathPrefix, slug);
285
+ // listing. Personal-mode paths are normalized in bucket-relative form.
286
+ const bucketPrefix = personalMode
287
+ ? normalizePersonalBrowsePrefix(pathPrefix)
288
+ : slug === undefined
289
+ ? pathPrefix
290
+ : toBucketRelative(pathPrefix, slug);
291
+ // Company callers can spell the bucket root as `/` or `.`. S3's empty
292
+ // Prefix is the root; passing either literal would list under that key.
293
+ const listPrefix = !personalMode && (bucketPrefix === "/" || bucketPrefix === ".")
294
+ ? ""
295
+ : bucketPrefix;
284
296
  // Pull the caller's explicit-grant graph once so per-key classification is
285
297
  // O(grants) without N round-trips. Retain the raw pattern after stripping
286
298
  // only the redundant company anchor: `foo/` and `foo/*` have different
@@ -220,9 +220,23 @@ export function createLaneMessageHandlers(deps) {
220
220
  storySwitchDeliveryStarted = true;
221
221
  opts.onAccepted?.();
222
222
  };
223
+ if (opts.text !== undefined && opts.textFile !== undefined) {
224
+ throw new LanesError("message_text_source_conflict", "Pass either --text or --text-file, not both", { lane_id: laneId });
225
+ }
223
226
  let text = opts.text;
224
- if (!text) {
225
- throw new LanesError("missing_text", "--text is required", {});
227
+ if (opts.textFile !== undefined) {
228
+ const textPath = path.isAbsolute(opts.textFile)
229
+ ? opts.textFile
230
+ : path.resolve(ctx.callerCwd(), opts.textFile);
231
+ try {
232
+ text = ctx.readFileSync(textPath, "utf8");
233
+ }
234
+ catch (err) {
235
+ throw new LanesError("message_text_file_unreadable", `Could not read message text file ${opts.textFile}: ${err instanceof Error ? err.message : String(err)}`, { lane_id: laneId, path: textPath });
236
+ }
237
+ }
238
+ if (text === undefined || text.length === 0) {
239
+ throw new LanesError("missing_text", "Pass non-empty message text with --text or --text-file", { lane_id: laneId });
226
240
  }
227
241
  let resolution;
228
242
  try {
@@ -261,6 +275,7 @@ export function createLaneMessageHandlers(deps) {
261
275
  : undefined;
262
276
  await dispatchOutpostLaneCommand(ctx, lane, "message", {
263
277
  ...opts,
278
+ text,
264
279
  ...(supersededMessageCallerId === undefined ? {} : { supersededMessageCallerId }),
265
280
  ...(supersededMessageSessionId === undefined ? {} : { supersededMessageSessionId }),
266
281
  }, {
@@ -35,6 +35,7 @@ export interface ResumeProfileRepinAuthorization {
35
35
  }
36
36
  export interface LaneMessageOptions {
37
37
  text?: string;
38
+ textFile?: string;
38
39
  project?: string;
39
40
  story?: string;
40
41
  compact?: boolean;
@@ -33,6 +33,7 @@ import { withCliFeatureUsed } from "../utils/cli-telemetry.js";
33
33
  import { resolveLanesHqRoot } from "../lib/lanes/hq-root.js";
34
34
  import { isSsmRunCommandProcess } from "../lib/lanes/ssm-command-context.js";
35
35
  import { readLanesRegistryFlag } from "../lib/lanes/registry-flag.js";
36
+ import { LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG } from "../lib/lanes/flag-keys.js";
36
37
  import { isControllerAliasOutpostId } from "../lib/lanes/admission-host.js";
37
38
  import { runLeadAutopilotPass } from "../lib/lanes/autopilot.js";
38
39
  import { syncOutpostLeadRegistries } from "../lib/lanes/autopilot-registry.js";
@@ -1197,14 +1198,20 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOpt
1197
1198
  throw new LanesError("outpost_lane_list_unavailable", `Could not read lanes from Outpost ${outpostId}: ${message}`, { outpost_id: outpostId });
1198
1199
  }
1199
1200
  if (result.done !== true || result.status !== "Success" || result.exitCode !== 0) {
1200
- throw new LanesError("outpost_lane_list_unavailable", `Outpost ${outpostId} did not return a successful lane list`, { outpost_id: outpostId, status: result.status, done: result.done ?? false, exit_code: result.exitCode });
1201
+ throw new LanesError("outpost_lane_list_unavailable", `Outpost ${outpostId} did not return a successful lane list`, {
1202
+ outpost_id: outpostId,
1203
+ command_id: result.commandId,
1204
+ status: result.status,
1205
+ done: result.done ?? false,
1206
+ exit_code: result.exitCode,
1207
+ });
1201
1208
  }
1202
1209
  let body;
1203
1210
  try {
1204
1211
  body = JSON.parse(result.stdout);
1205
1212
  }
1206
1213
  catch (err) {
1207
- throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned invalid lane-list JSON: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId });
1214
+ throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned invalid lane-list JSON: ${dispatchErrorMessage(err)}`, { outpost_id: outpostId, command_id: result.commandId });
1208
1215
  }
1209
1216
  if (body === null || typeof body !== "object" || Array.isArray(body) ||
1210
1217
  body.ok !== true ||
@@ -1212,6 +1219,7 @@ async function readOutpostLaneRecordsUncached(ctx, outpostId, query, dispatchOpt
1212
1219
  !Array.isArray(body.lanes)) {
1213
1220
  throw new LanesError("outpost_lane_list_invalid", `Outpost ${outpostId} returned a malformed lane list`, {
1214
1221
  outpost_id: outpostId,
1222
+ command_id: result.commandId,
1215
1223
  });
1216
1224
  }
1217
1225
  const listed = body.lanes;
@@ -1499,7 +1507,12 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1499
1507
  }
1500
1508
  const separator = laneId.lastIndexOf("_");
1501
1509
  const hostSuffix = separator < 0 ? "" : laneId.slice(separator + 1);
1502
- if (!hostSuffix || sanitizeHostComponent(hostSuffix) !== hostSuffix) {
1510
+ const ownerCompany = !hostSuffix && requestedUlid !== undefined && options.allowHostlessOwnerLookup
1511
+ ? await options.allowHostlessOwnerLookup()
1512
+ : undefined;
1513
+ const hostlessUlidLookup = ownerCompany !== undefined;
1514
+ if ((!hostSuffix && !hostlessUlidLookup) ||
1515
+ (hostSuffix && sanitizeHostComponent(hostSuffix) !== hostSuffix)) {
1503
1516
  throw commandLaneNotFound(laneId, options.outpostHint === true);
1504
1517
  }
1505
1518
  let rawOutpostRoutes;
@@ -1542,7 +1555,9 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1542
1555
  if (requestedOutpostId !== undefined && !outpostIds.includes(requestedOutpostId)) {
1543
1556
  throw new LanesError("outpost_not_registered", `Outpost ${requestedOutpostId} is not registered`, { lane_id: laneId, outpost_id: requestedOutpostId });
1544
1557
  }
1545
- const hostRoutes = outpostRoutes.filter((route) => outpostRouteMatchesHost(route, hostSuffix));
1558
+ const hostRoutes = hostlessUlidLookup
1559
+ ? []
1560
+ : outpostRoutes.filter((route) => outpostRouteMatchesHost(route, hostSuffix));
1546
1561
  const identity = ctx.readOutpostIdentity();
1547
1562
  const localHostRoutes = outpostRoutes.filter((route) => outpostRouteMatchesHost(route, ctx.hostname()));
1548
1563
  if (localHostRoutes.length > 1 || (identity.kind === "id" && localHostRoutes.some((route) => route.outpostId !== identity.id))) {
@@ -1553,20 +1568,22 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1553
1568
  const hostOutpostIdSet = new Set(hostOutpostIds);
1554
1569
  const matches = [];
1555
1570
  const failedOutpostIds = [];
1571
+ const failedOutpostCommands = [];
1556
1572
  const probedOutpostIds = [];
1557
1573
  const lookupOutpostIds = requestedOutpostId !== undefined
1558
1574
  ? [requestedOutpostId]
1559
- : [
1560
- ...hostOutpostIds.filter((id) => id !== localOutpostId),
1561
- ...outpostIds.filter((id) => id !== localOutpostId && !hostOutpostIdSet.has(id)),
1562
- ];
1575
+ : hostlessUlidLookup
1576
+ ? outpostIds.filter((id) => id !== localOutpostId)
1577
+ : [
1578
+ ...hostOutpostIds.filter((id) => id !== localOutpostId),
1579
+ ...outpostIds.filter((id) => id !== localOutpostId && !hostOutpostIdSet.has(id)),
1580
+ ];
1563
1581
  for (const outpostId of lookupOutpostIds) {
1564
1582
  let ownerLanes;
1565
1583
  try {
1566
- ownerLanes = await readOutpostLaneRecords(ctx, outpostId, {
1567
- hierarchy: [laneId],
1568
- snapshot: true,
1569
- });
1584
+ ownerLanes = await readOutpostLaneRecords(ctx, outpostId, hostlessUlidLookup
1585
+ ? { snapshot: true }
1586
+ : { hierarchy: [laneId], snapshot: true });
1570
1587
  probedOutpostIds.push(outpostId);
1571
1588
  }
1572
1589
  catch (err) {
@@ -1575,16 +1592,38 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1575
1592
  if (isLanesError(err) && err.error === "outpost_wait_invalid")
1576
1593
  throw err;
1577
1594
  failedOutpostIds.push(outpostId);
1595
+ failedOutpostCommands.push({
1596
+ outpost_id: outpostId,
1597
+ ...(isLanesError(err) && typeof err.details.command_id === "string"
1598
+ ? { command_id: err.details.command_id }
1599
+ : {}),
1600
+ });
1578
1601
  continue;
1579
1602
  }
1580
1603
  for (const lane of ownerLanes) {
1581
- if (lane.lane_id === laneId &&
1582
- lane.host.kind === "local" &&
1583
- sanitizeHostComponent(lane.host.id) === hostSuffix) {
1604
+ const recordedSuffix = lane.lane_id.slice(lane.lane_id.lastIndexOf("_") + 1);
1605
+ const laneCompany = hostlessUlidLookup
1606
+ ? resolveLaneCompany(ctx.hqRoot(), lane).company
1607
+ : undefined;
1608
+ const companyMatches = !hostlessUlidLookup || (ownerCompany !== undefined &&
1609
+ laneCompany?.uid === ownerCompany.uid &&
1610
+ laneCompany.slug === ownerCompany.slug);
1611
+ if (lane.host.kind === "local" && ((!hostlessUlidLookup && lane.lane_id === laneId &&
1612
+ sanitizeHostComponent(lane.host.id) === hostSuffix) ||
1613
+ (hostlessUlidLookup && laneUlidPart(lane.lane_id) === requestedUlid &&
1614
+ sanitizeHostComponent(lane.host.id) === recordedSuffix &&
1615
+ companyMatches))) {
1584
1616
  matches.push({ outpostId, lane });
1585
1617
  }
1586
1618
  }
1587
1619
  }
1620
+ if (hostlessUlidLookup && failedOutpostIds.length > 0) {
1621
+ throw new LanesError("lane_owner_lookup_unavailable", `Could not verify whether lane ${laneId} is registered on every Outpost`, {
1622
+ lane_id: laneId,
1623
+ failed_outpost_ids: failedOutpostIds,
1624
+ remote_commands: failedOutpostCommands,
1625
+ });
1626
+ }
1588
1627
  if (matches.length === 0) {
1589
1628
  throw commandLaneNotFound(laneId, options.outpostHint === true, {
1590
1629
  ...(failedOutpostIds.length > 0 ? { failed_outpost_ids: failedOutpostIds } : {}),
@@ -1604,9 +1643,9 @@ async function resolveCommandLaneTarget(ctx, laneId, options = {}) {
1604
1643
  }
1605
1644
  function laneUlidPart(laneId) {
1606
1645
  const separator = laneId.indexOf("_");
1607
- if (separator !== 26)
1646
+ if (separator !== -1 && separator !== 26)
1608
1647
  return undefined;
1609
- const ulid = laneId.slice(0, separator);
1648
+ const ulid = separator === -1 ? laneId : laneId.slice(0, separator);
1610
1649
  return /^[0-9A-HJKMNP-TV-Z]{26}$/i.test(ulid) ? ulid.toUpperCase() : undefined;
1611
1650
  }
1612
1651
  function commandLaneNotFound(laneId, outpostHint, details = {}) {
@@ -7660,7 +7699,7 @@ function writeAnswerDeliveryFailed(hqRoot, laneId, questionId, err, at) {
7660
7699
  async function runReconcile(ctx, opts, recoverQueuedMessages) {
7661
7700
  const fromEnv = ctx.env.HQ_ROOT?.trim();
7662
7701
  const hqRoot = fromEnv && ctx.existsSync(fromEnv) ? fromEnv : ctx.hqRoot();
7663
- const mirrorRefresh = opts.dryRun
7702
+ const mirrorRefresh = opts.dryRun || opts.terminalKeepAliveCandidate
7664
7703
  ? {
7665
7704
  updated: new Map(),
7666
7705
  unverifiedRemote: new Map(),
@@ -7724,6 +7763,9 @@ async function runReconcile(ctx, opts, recoverQueuedMessages) {
7724
7763
  recoverQueuedMessages,
7725
7764
  mesh: ctx.mesh,
7726
7765
  env: ctx.env,
7766
+ ...(opts.terminalKeepAliveCandidate !== undefined
7767
+ ? { terminalKeepAliveCandidate: opts.terminalKeepAliveCandidate }
7768
+ : {}),
7727
7769
  prepareKeepAliveClaudeResume: async (lane) => {
7728
7770
  const recoveredLane = laneWithRecoveredProviderSession(hqRoot, lane);
7729
7771
  const pendingModel = recoveredLane.pending_settings?.model;
@@ -8041,7 +8083,16 @@ export function registerLanesCommand(parent, deps = {}) {
8041
8083
  persistClaudeAccountSelection,
8042
8084
  printJson,
8043
8085
  recordResumeSetup,
8044
- resolveCommandLane: (commandCtx, laneId) => resolveCommandLaneTarget(commandCtx, laneId),
8086
+ resolveCommandLane: (commandCtx, laneId) => resolveCommandLaneTarget(commandCtx, laneId, {
8087
+ allowHostlessOwnerLookup: async () => {
8088
+ const company = await commandCtx.resolveCompany();
8089
+ const enabled = await readLanesRegistryFlag(LANE_MESSAGE_ROUTE_OWNER_HOST_FLAG, {
8090
+ company,
8091
+ env: commandCtx.env,
8092
+ });
8093
+ return enabled ? company : undefined;
8094
+ },
8095
+ }),
8045
8096
  resolveActiveQuestionCompany,
8046
8097
  hasPendingQuestionForLane: questionHandlers.hasPendingQuestionForLane,
8047
8098
  cancelFallbackQuestionsForLane: questionHandlers.cancelFallbackQuestionsForLane,
@@ -8406,7 +8457,8 @@ export function registerLanesCommand(parent, deps = {}) {
8406
8457
  .command("message")
8407
8458
  .description("Send a follow-up message to a lane")
8408
8459
  .argument("<lane-id>", "Lane id")
8409
- .requiredOption("--text <msg>", "Message text")
8460
+ .option("--text <msg>", "Message text")
8461
+ .option("--text-file <path>", "Read message text from a file")
8410
8462
  .option("--project <slug>", "Re-point project attribution; a changed story is named in the resumed prompt")
8411
8463
  .option("--story <id>", "Re-point story attribution; a changed story is named in the resumed prompt")
8412
8464
  .option("--account <id>", "Move the lane's session to this account of its provider before the message resumes it")
@@ -8745,6 +8797,30 @@ export function registerLanesCommand(parent, deps = {}) {
8745
8797
  await messageHandlers.recoverQueuedMessages(ctx, lane.lane_id);
8746
8798
  });
8747
8799
  }));
8800
+ lanes
8801
+ .command("_keep-alive-terminal-trigger <laneId> <version> <dueAt>", { hidden: true })
8802
+ .action(wrap(async (laneId, versionText, dueAt) => {
8803
+ const version = Number.parseInt(versionText, 10);
8804
+ if (!Number.isSafeInteger(version) || version < 1 || String(version) !== versionText) {
8805
+ throw new LanesError("invalid_lane_version", "Terminal keep-alive trigger requires a positive lane version", {
8806
+ lane_id: laneId,
8807
+ version: versionText,
8808
+ });
8809
+ }
8810
+ const dueAtMs = Date.parse(dueAt);
8811
+ if (!Number.isFinite(dueAtMs)) {
8812
+ throw new LanesError("invalid_terminal_trigger_time", "Terminal keep-alive trigger requires a valid due time", {
8813
+ lane_id: laneId,
8814
+ due_at: dueAt,
8815
+ });
8816
+ }
8817
+ const delayMs = dueAtMs - Date.now();
8818
+ if (delayMs > 0)
8819
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
8820
+ await runReconcile(ctx, { terminalKeepAliveCandidate: { laneId, version } }, async (lane) => {
8821
+ await messageHandlers.recoverQueuedMessages(ctx, lane.lane_id);
8822
+ });
8823
+ }));
8748
8824
  reconcile
8749
8825
  .command("install")
8750
8826
  .description("Write systemd user unit and timer files (idempotent)")
@@ -0,0 +1,3 @@
1
+ export declare const VAULT_RETENTION_SETTINGS_FLAG = "cli.vault-retention-settings";
2
+ export declare function vaultRetentionSettingsEnabled(companyUid?: string, idToken?: string): boolean | Promise<boolean>;
3
+ //# sourceMappingURL=vault-retention-flag.d.ts.map
@@ -0,0 +1,9 @@
1
+ import { resolveCompanyFlagGate, resolveProcessFlagGate } from "../lib/flag-registry.js";
2
+ export const VAULT_RETENTION_SETTINGS_FLAG = "cli.vault-retention-settings";
3
+ export function vaultRetentionSettingsEnabled(companyUid, idToken) {
4
+ if (companyUid && idToken) {
5
+ return resolveCompanyFlagGate(companyUid, idToken, VAULT_RETENTION_SETTINGS_FLAG, { fallback: false }, () => false);
6
+ }
7
+ return resolveProcessFlagGate(VAULT_RETENTION_SETTINGS_FLAG, { fallback: false }, () => false);
8
+ }
9
+ //# sourceMappingURL=vault-retention-flag.js.map
@@ -0,0 +1,11 @@
1
+ import { Command } from "commander";
2
+ import { vaultApiFetch } from "../utils/vault-api.js";
3
+ type Dependencies = {
4
+ enabled: (companyUid?: string, token?: string) => boolean | Promise<boolean>;
5
+ token: () => Promise<string>;
6
+ companyUid: (token: string, slug: string | undefined) => Promise<string>;
7
+ request: typeof vaultApiFetch;
8
+ };
9
+ export declare function registerVaultRetentionCommand(program: Command, deps?: Dependencies): void;
10
+ export {};
11
+ //# sourceMappingURL=vault-retention.d.ts.map
@@ -0,0 +1,69 @@
1
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
2
+ import { markExpectedUserError } from "../utils/expected-cli-error.js";
3
+ import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
4
+ import { vaultRetentionSettingsEnabled } from "./vault-retention-flag.js";
5
+ const defaultDependencies = {
6
+ enabled: vaultRetentionSettingsEnabled,
7
+ token: ensureCognitoToken,
8
+ companyUid: getCompanyUid,
9
+ request: vaultApiFetch,
10
+ };
11
+ export function registerVaultRetentionCommand(program, deps = defaultDependencies) {
12
+ const vault = program.command("vault").description("Manage HQ vault settings");
13
+ const retention = vault
14
+ .command("retention")
15
+ .description("Get or set the default retention for future vault object versions")
16
+ .option("--company <slug>", "Company vault slug (omit for your personal vault)");
17
+ const companyOf = (command) => command.opts().company ??
18
+ retention.opts().company;
19
+ retention
20
+ .command("get")
21
+ .description("Show the vault's default retention in days")
22
+ .action(async function () {
23
+ await execute(this, "GET");
24
+ });
25
+ retention
26
+ .command("set")
27
+ .description("Set the vault's default retention in days (1-30)")
28
+ .argument("<days>")
29
+ .action(async function (rawDays) {
30
+ const days = Number(rawDays);
31
+ if (!Number.isInteger(days) || days < 1 || days > 30) {
32
+ throw new Error("days must be an integer from 1 through 30");
33
+ }
34
+ await execute(this, "PUT", days);
35
+ });
36
+ async function execute(command, method, days) {
37
+ const slug = companyOf(command);
38
+ let token;
39
+ let companyUid;
40
+ if (slug) {
41
+ token = await deps.token();
42
+ companyUid = await deps.companyUid(token, slug);
43
+ if (!(await deps.enabled(companyUid, token))) {
44
+ throw markExpectedUserError(new Error("Vault retention settings are not enabled for this company."));
45
+ }
46
+ }
47
+ else {
48
+ if (!(await deps.enabled())) {
49
+ throw markExpectedUserError(new Error("Vault retention settings are not enabled for this company."));
50
+ }
51
+ token = await deps.token();
52
+ }
53
+ const query = companyUid ? { companyUid } : undefined;
54
+ const response = await deps.request({
55
+ token,
56
+ path: "/vault/retention",
57
+ method,
58
+ ...(query ? { query } : {}),
59
+ ...(days === undefined ? {} : { body: { days } }),
60
+ });
61
+ if (!response.ok) {
62
+ const body = (await response.json().catch(() => ({})));
63
+ throw new Error(body.error ?? body.message ?? response.statusText);
64
+ }
65
+ const body = (await response.json());
66
+ process.stdout.write(`${body.days} days\n`);
67
+ }
68
+ }
69
+ //# sourceMappingURL=vault-retention.js.map
package/dist/fast-core.js CHANGED
@@ -59,12 +59,13 @@ export async function runFastCore(argv) {
59
59
  if (typeof handler !== "function") {
60
60
  throw new Error(`Native fast handler ${entry.exportName} was not exported`);
61
61
  }
62
+ const readsStdin = entry.readsStdinFor?.(parsed.args) ?? entry.readsStdin ?? false;
62
63
  const chunks = [];
63
- if (entry.readsStdin) {
64
+ if (readsStdin) {
64
65
  for await (const chunk of process.stdin)
65
66
  chunks.push(chunk);
66
67
  }
67
- const code = await handler(parsed.args, entry.readsStdin ? Buffer.concat(chunks).toString("utf8") : "", process.cwd(), hqRoot, entry.name.split(" ").at(-1) ?? entry.name, parsed.hqRootArg);
68
+ const code = await handler(parsed.args, readsStdin ? Buffer.concat(chunks).toString("utf8") : "", process.cwd(), hqRoot, entry.name.split(" ").at(-1) ?? entry.name, parsed.hqRootArg);
68
69
  // Let Node drain handler output before terminating. The timeout guard's
69
70
  // stderr message must survive the native route's process boundary.
70
71
  process.exitCode = code;
@@ -80,12 +81,18 @@ export async function runFastCore(argv) {
80
81
  : process.cwd();
81
82
  const injectRoot = entry.kind === "shell" || entry.kind === "native" &&
82
83
  (entry.shellHqRoot === "live" || entry.shellHqRoot === "reconcile-default" && shouldInjectRoot);
84
+ const childEnv = {
85
+ ...(entry.name === "hq-session" ? { HQ_HQ_SESSION_NO_CLI: "1" } : {}),
86
+ ...(entry.name === "migrate-policy-triggers"
87
+ ? { CLAUDE_PROJECT_DIR: process.env.CLAUDE_PROJECT_DIR || process.cwd() }
88
+ : {}),
89
+ };
83
90
  const { code } = runBundledScript({
84
91
  asset,
85
92
  args: parsed.args,
86
93
  cwd,
87
94
  ...(injectRoot ? { hqRoot } : {}),
88
- env: entry.name === "hq-session" ? { HQ_HQ_SESSION_NO_CLI: "1" } : undefined,
95
+ ...(Object.keys(childEnv).length ? { env: childEnv } : {}),
89
96
  });
90
97
  // Propagate verbatim: these scripts use their exit code as their interface.
91
98
  process.exit(code);
@@ -40,6 +40,8 @@ export type FastScaffoldScript = {
40
40
  shellHqRoot?: "none" | "live" | "reconcile-default";
41
41
  /** Whether this handler consumes stdin. Shell companions inherit stdin directly. */
42
42
  readsStdin?: boolean;
43
+ /** Per-invocation stdin policy for handlers whose shell reads conditionally. */
44
+ readsStdinFor?: (args: readonly string[]) => boolean;
43
45
  /** Whether the bundled fast-route module closure needs Node's network timeout patch. */
44
46
  requiresNodeNetworkCompatibility?: boolean;
45
47
  };
@@ -184,6 +184,17 @@ export const FAST_SCAFFOLD_SCRIPTS = [
184
184
  shellCwd: "live",
185
185
  shellHqRoot: "live",
186
186
  },
187
+ ...[
188
+ { name: "eval-trigger", modulePath: "./commands/core-eval-trigger-native.js", exportName: "runEvalTriggerFast", asset: "core/scripts/eval-trigger.sh", root: "cwd", shellCwd: "caller", shellHqRoot: "none", readsStdinFor: (args) => args[0] === "--check" },
189
+ { name: "migrate-policy-triggers", modulePath: "./commands/core-migrate-policy-triggers-native.js", exportName: "runMigratePolicyTriggersFast", asset: "core/scripts/migrate-policy-triggers.sh", root: "live", shellCwd: "caller", shellHqRoot: "live" },
190
+ { name: "register-project", modulePath: "./commands/core-register-project-native.js", exportName: "runRegisterProjectFast", asset: "core/scripts/register-project.sh", root: "live", shellCwd: "caller", shellHqRoot: "live" },
191
+ { name: "work-mesh-live-rebind", modulePath: "./commands/core-work-mesh-live-rebind-native.js", exportName: "runWorkMeshLiveRebindFast", asset: "core/scripts/work-mesh-live-rebind.sh", root: "live", shellCwd: "caller", shellHqRoot: "live" },
192
+ ].map((entry) => ({
193
+ ...entry,
194
+ kind: "native",
195
+ flagKey: "hq-core-native-fast-route",
196
+ familyFlagKey: "core-native-hook-helpers",
197
+ })),
187
198
  ...["resolve", "pickup", "repo", "secrets", "transfer"].map((name) => ({
188
199
  name: `delegate ${name}`,
189
200
  kind: "native",
@@ -1,6 +1,6 @@
1
1
  export declare const AGENT_MODEL_PROVIDERS: readonly ["codex", "grok", "claude"];
2
2
  export type AgentModelProvider = (typeof AGENT_MODEL_PROVIDERS)[number];
3
- export interface AgentCatalogModel {
3
+ interface AgentCatalogModel {
4
4
  id: string;
5
5
  label: string;
6
6
  visibility: string;
@@ -10,7 +10,7 @@ export interface AgentCatalogModel {
10
10
  selectable: boolean;
11
11
  unavailableReason?: string | null;
12
12
  }
13
- export interface AgentCatalogProvider {
13
+ interface AgentCatalogProvider {
14
14
  models: AgentCatalogModel[];
15
15
  defaultModel: string;
16
16
  }
@@ -58,4 +58,5 @@ export declare function validateAgentModelSelection(catalog: AgentModelCatalog,
58
58
  export declare function modelSelectionErrorMessage(modelId: string, result: Extract<ModelSelectionResult, {
59
59
  ok: false;
60
60
  }>): string;
61
+ export {};
61
62
  //# sourceMappingURL=model-catalog.d.ts.map
@@ -1,12 +1,4 @@
1
1
  import * as fs from "node:fs";
2
- export interface CompanySkillWrapperOwnershipDiagnostics {
3
- kind: "directory" | "symlink" | "file" | "other";
4
- bridgeState: "all_live" | "partial" | "none" | "empty" | "not_applicable";
5
- markerState: "bound" | "missing" | "invalid" | "unbound" | "unavailable" | "not_applicable";
6
- linkTarget: "company_skills" | "outside_company_skills" | "unavailable" | "not_applicable";
7
- platform: "win32" | "darwin" | "linux" | "other";
8
- nameEncoding: "percent_encoded" | "literal";
9
- }
10
2
  export declare function companySkillWrapperSentryTagsFor(error: unknown): Record<string, string> | undefined;
11
3
  export interface SurfaceCompanySkillInput {
12
4
  hqRoot: string;
@@ -1,4 +1,4 @@
1
- export type BridgeCommand = "status" | "install" | "doctor";
1
+ type BridgeCommand = "status" | "install" | "doctor";
2
2
  export type BridgeOptions = {
3
3
  command: BridgeCommand;
4
4
  hqRoot: string;
@@ -28,4 +28,5 @@ export declare function isHqRootLike(candidate: string): boolean;
28
28
  export declare function outputStyleSlug(name: string): string;
29
29
  /** Runs one bridge command; returns the process exit code. */
30
30
  export declare function runCodexSkillBridge(options: BridgeOptions, io: BridgeIo): number;
31
+ export {};
31
32
  //# sourceMappingURL=codex-skill-bridge.d.ts.map