@indigoai-us/hq-cli 5.345.170 → 5.345.172

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 (142) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/assets/scaffold/core/scripts/hq-delegate-send.sh +1 -1
  3. package/dist/command-catalog.generated.d.ts +206 -7
  4. package/dist/command-catalog.generated.js +252 -8
  5. package/dist/command-registration-plan.d.ts +7 -0
  6. package/dist/command-registration-plan.js +1 -0
  7. package/dist/commands/access.d.ts +32 -1
  8. package/dist/commands/access.js +112 -3
  9. package/dist/commands/agent-kit.d.ts +1 -1
  10. package/dist/commands/agent-kit.js +3 -1
  11. package/dist/commands/agents.js +2 -1
  12. package/dist/commands/bot.d.ts +3 -0
  13. package/dist/commands/bot.js +179 -18
  14. package/dist/commands/cloud.d.ts +58 -1
  15. package/dist/commands/cloud.js +228 -10
  16. package/dist/commands/core-company-index-scope-flag.d.ts +8 -1
  17. package/dist/commands/core-company-index-scope-flag.js +9 -2
  18. package/dist/commands/core-delegate-w1b.js +5 -2
  19. package/dist/commands/core-dm-bind-native.js +19 -4
  20. package/dist/commands/core-rebuild-index-fast.js +24 -6
  21. package/dist/commands/core.js +18 -6
  22. package/dist/commands/daemon.js +16 -1
  23. package/dist/commands/dm.d.ts +17 -3
  24. package/dist/commands/dm.js +121 -26
  25. package/dist/commands/files.d.ts +1 -0
  26. package/dist/commands/files.js +22 -4
  27. package/dist/commands/integrations-api.d.ts +4 -0
  28. package/dist/commands/integrations-api.js +18 -2
  29. package/dist/commands/integrations-connect.d.ts +10 -3
  30. package/dist/commands/integrations-connect.js +70 -6
  31. package/dist/commands/integrations-core.d.ts +23 -0
  32. package/dist/commands/integrations-core.js +100 -0
  33. package/dist/commands/integrations-manage.js +8 -1
  34. package/dist/commands/integrations.js +49 -5
  35. package/dist/commands/lanes-loop.d.ts +20 -0
  36. package/dist/commands/lanes-loop.js +69 -11
  37. package/dist/commands/lanes.js +9 -1
  38. package/dist/commands/rescue.d.ts +9 -0
  39. package/dist/commands/rescue.js +31 -0
  40. package/dist/commands/run.js +115 -40
  41. package/dist/commands/scan.d.ts +5 -1
  42. package/dist/commands/scan.js +16 -4
  43. package/dist/commands/secrets.js +50 -9
  44. package/dist/commands/sync-core.d.ts +22 -0
  45. package/dist/commands/sync-core.js +124 -0
  46. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2447 -2135
  47. package/dist/hq-cloud-sync-runner/package.json +1 -1
  48. package/dist/lib/agent-kit/inbox-state.d.ts +17 -0
  49. package/dist/lib/agent-kit/inbox-state.js +41 -20
  50. package/dist/lib/agent-kit/kit-config.d.ts +2 -0
  51. package/dist/lib/agent-kit/kit-config.js +6 -0
  52. package/dist/lib/agent-kit/mcp/tools.js +12 -5
  53. package/dist/lib/agent-kit/run/heartbeat.d.ts +6 -3
  54. package/dist/lib/agent-kit/run/heartbeat.js +6 -3
  55. package/dist/lib/agent-kit/run/inbox.d.ts +8 -3
  56. package/dist/lib/agent-kit/run/inbox.js +42 -10
  57. package/dist/lib/agent-kit/run/request-timeout.d.ts +26 -0
  58. package/dist/lib/agent-kit/run/request-timeout.js +51 -0
  59. package/dist/lib/bot/api.d.ts +1 -1
  60. package/dist/lib/bot/config.d.ts +10 -0
  61. package/dist/lib/bot/config.js +17 -1
  62. package/dist/lib/bot/fleet-local.d.ts +142 -0
  63. package/dist/lib/bot/fleet-local.js +689 -0
  64. package/dist/lib/bot/fleet-python.d.ts +47 -0
  65. package/dist/lib/bot/fleet-python.js +218 -0
  66. package/dist/lib/bot/index.d.ts +2 -0
  67. package/dist/lib/bot/index.js +2 -0
  68. package/dist/lib/bot/local-config.d.ts +3 -2
  69. package/dist/lib/bot/local-config.js +5 -2
  70. package/dist/lib/bot/promote.d.ts +10 -0
  71. package/dist/lib/bot/promote.js +3 -2
  72. package/dist/lib/bot/prompt.d.ts +6 -1
  73. package/dist/lib/bot/prompt.js +8 -3
  74. package/dist/lib/bot/runtime/index.js +8 -4
  75. package/dist/lib/bot/runtime/scratch-dir.d.ts +24 -0
  76. package/dist/lib/bot/runtime/scratch-dir.js +41 -0
  77. package/dist/lib/caller-principal.d.ts +4 -0
  78. package/dist/lib/caller-principal.js +40 -0
  79. package/dist/lib/capacity/collectors/grok.js +8 -2
  80. package/dist/lib/cli-flag-keys.d.ts +2 -0
  81. package/dist/lib/cli-flag-keys.js +2 -0
  82. package/dist/lib/daemon/catalog.d.ts +12 -4
  83. package/dist/lib/daemon/catalog.js +70 -24
  84. package/dist/lib/daemon/config.d.ts +0 -7
  85. package/dist/lib/daemon/config.js +0 -32
  86. package/dist/lib/daemon/run.d.ts +5 -1
  87. package/dist/lib/daemon/run.js +48 -11
  88. package/dist/lib/daemon/supervisor.js +3 -1
  89. package/dist/lib/doctor/checks/release-links.d.ts +62 -1
  90. package/dist/lib/doctor/checks/release-links.js +188 -2
  91. package/dist/lib/doctor/fix/apply.js +54 -0
  92. package/dist/lib/doctor/fix/remediation.d.ts +8 -1
  93. package/dist/lib/doctor/fix/remediation.js +22 -0
  94. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  95. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  96. package/dist/lib/index-render/index.d.ts +9 -0
  97. package/dist/lib/index-render/index.js +65 -5
  98. package/dist/lib/index-render/session-scope.d.ts +69 -0
  99. package/dist/lib/index-render/session-scope.js +279 -0
  100. package/dist/lib/lanes/admission.js +7 -20
  101. package/dist/lib/lanes/reconcile.js +13 -15
  102. package/dist/lib/lanes/stale-lock.d.ts +50 -0
  103. package/dist/lib/lanes/stale-lock.js +105 -0
  104. package/dist/lib/lanes/store.js +11 -76
  105. package/dist/lib/notify-recipient.d.ts +10 -4
  106. package/dist/lib/notify-recipient.js +16 -4
  107. package/dist/lib/secret-input-link.d.ts +42 -0
  108. package/dist/lib/secret-input-link.js +93 -0
  109. package/dist/lib/secret-scan/range.d.ts +57 -0
  110. package/dist/lib/secret-scan/range.js +266 -0
  111. package/dist/lib/secret-scan/repo-config.d.ts +18 -3
  112. package/dist/lib/secret-scan/repo-config.js +135 -2
  113. package/dist/lib/sync-core/engine-home.d.ts +44 -0
  114. package/dist/lib/sync-core/engine-home.js +193 -0
  115. package/dist/lib/sync-core/flag.d.ts +17 -0
  116. package/dist/lib/sync-core/flag.js +47 -0
  117. package/dist/lib/sync-core/reconcile.d.ts +31 -0
  118. package/dist/lib/sync-core/reconcile.js +196 -0
  119. package/dist/lib/sync-core/release.d.ts +32 -0
  120. package/dist/lib/sync-core/release.js +140 -0
  121. package/dist/lib/sync-core/rollout-state.d.ts +24 -0
  122. package/dist/lib/sync-core/rollout-state.js +94 -0
  123. package/dist/lib/vault-sync-switches.d.ts +5 -0
  124. package/dist/lib/vault-sync-switches.js +43 -0
  125. package/dist/run/script-bound-schema.d.ts +27 -0
  126. package/dist/run/script-bound-schema.js +97 -0
  127. package/dist/secrets-env-action.d.ts +2 -0
  128. package/dist/secrets-env-action.js +7 -2
  129. package/dist/utils/cognito-session.d.ts +3 -30
  130. package/dist/utils/cognito-session.js +2 -100
  131. package/dist/utils/machine-principal-state.d.ts +38 -0
  132. package/dist/utils/machine-principal-state.js +110 -0
  133. package/dist/utils/remote-list-not-converging.d.ts +21 -0
  134. package/dist/utils/remote-list-not-converging.js +40 -0
  135. package/dist/utils/safe-link.js +9 -0
  136. package/dist/utils/script-bound-exec.d.ts +158 -0
  137. package/dist/utils/script-bound-exec.js +568 -0
  138. package/dist/utils/sync-watchdog.d.ts +92 -0
  139. package/dist/utils/sync-watchdog.js +250 -0
  140. package/package.json +8 -8
  141. package/dist/lib/daemon/sync-core-canary.d.ts +0 -9
  142. package/dist/lib/daemon/sync-core-canary.js +0 -56
@@ -13,13 +13,15 @@
13
13
  * hq sync status — show local journal summary
14
14
  */
15
15
  import { isAccessDeniedError, accessDeniedKeyOf, formatAccessDenied, } from "../utils/access-denied-hint.js";
16
+ import { describeSyncError, formatRemoteListNotConverging, } from "../utils/remote-list-not-converging.js";
16
17
  import { findReadOnlyLocalOnly, isGrantEnforced, readVaultAccessManifest, } from "../lib/sync/read-only-local-only.js";
17
18
  import { SERVER_OWNED_FOLDERS, findServerOwnedLocalOnly, } from "../lib/sync/server-owned-local-only.js";
18
19
  import chalk from "chalk";
19
20
  import * as fs from "fs";
20
21
  import * as path from "path";
21
- import { share, getStateDir, listJournals, createIgnoreFilter, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, syncDoctor, } from "@indigoai-us/hq-cloud";
22
+ import { share, getStateDir, listJournals, createIgnoreFilter, VaultClient, computePersonalVaultPaths, PERSONAL_VAULT_JOURNAL_SLUG, resolvePullScope, readSyncProgress, syncDoctor, } from "@indigoai-us/hq-cloud";
22
23
  import { syncLeg } from "../utils/sync-operation-lock.js";
24
+ import { SYNC_WATCHDOG_EXIT_CODE, SYNC_WATCHDOG_REPORT_GRACE_MS, SyncWatchdogTimeoutError, createSnapshotProgressProbe, resolveSyncWatchdogLimits, withSyncWatchdog, } from "../utils/sync-watchdog.js";
23
25
  import { DEFAULT_HQ_ROOT, ensureCognitoToken, buildVaultConfig, loadCachedTokens, } from "../utils/cognito-session.js";
24
26
  import { resolveUploadAuthor } from "../utils/upload-author.js";
25
27
  import { companyFolderExceedsThreshold, emitNarrowHint, isStrictRefusal, resolveBannerLevel, resolveNarrowHintPresentationLevel, resolveNarrowHintMinBytes, } from "../lib/narrow-hint-banner.js";
@@ -69,7 +71,13 @@ async function ensureSyncCognitoToken() {
69
71
  * deliberately duck-typed so hq-cli remains compatible while hq-cloud and the
70
72
  * CLI are released independently.
71
73
  */
72
- export function formatSyncFailure(err) {
74
+ export function formatSyncFailure(err, ctx = {}) {
75
+ const unreachable = formatUnreachablePushPaths(err, ctx);
76
+ if (unreachable)
77
+ return unreachable;
78
+ const remoteList = formatRemoteListNotConverging(err);
79
+ if (remoteList)
80
+ return remoteList;
73
81
  // A plan-limit refusal the runner threw (a 402 on the whole request) renders
74
82
  // with the same reason, fix options and upgrade link as every other refusal.
75
83
  const planGate = planGateErrorFromVaultError(err);
@@ -105,6 +113,150 @@ export function formatSyncFailure(err) {
105
113
  }
106
114
  return err instanceof Error ? err.message : String(err);
107
115
  }
116
+ function asUnreachablePushPaths(err) {
117
+ if (!(err instanceof Error) || err.name !== "UnreachablePushPathsError")
118
+ return null;
119
+ const candidate = err;
120
+ if (!Array.isArray(candidate.paths))
121
+ return null;
122
+ if (typeof candidate.reasons !== "object" || candidate.reasons === null)
123
+ return null;
124
+ const paths = candidate.paths.filter((p) => typeof p === "string");
125
+ return {
126
+ message: err.message,
127
+ paths,
128
+ reasons: candidate.reasons,
129
+ };
130
+ }
131
+ /**
132
+ * The `.hqinclude` line that admits a named path: its HQ-relative spelling,
133
+ * with a trailing slash for a folder. Resolved the way the push resolver
134
+ * reads a named path (absolute, HQ root, the company folder, then the current
135
+ * directory); a path that resolves nowhere inside the HQ tree is suggested as
136
+ * typed.
137
+ */
138
+ export function hqincludeEntryFor(namedPath, ctx = {}) {
139
+ const toEntry = (rel, isDir) => {
140
+ const posix = rel.split(path.sep).join("/").replace(/\/+$/, "");
141
+ return isDir ? posix + "/" : posix;
142
+ };
143
+ const hqRoot = ctx.hqRoot ? path.resolve(ctx.hqRoot) : undefined;
144
+ if (hqRoot) {
145
+ const candidates = path.isAbsolute(namedPath)
146
+ ? [namedPath]
147
+ : [
148
+ path.join(hqRoot, namedPath),
149
+ ...(ctx.companySlug
150
+ ? [path.join(hqRoot, "companies", ctx.companySlug, namedPath)]
151
+ : []),
152
+ path.resolve(ctx.cwd ?? process.cwd(), namedPath),
153
+ ];
154
+ for (const candidate of candidates) {
155
+ const rel = path.relative(hqRoot, candidate);
156
+ if (rel === "" ||
157
+ rel === ".." ||
158
+ rel.startsWith(".." + path.sep) ||
159
+ path.isAbsolute(rel)) {
160
+ continue;
161
+ }
162
+ let stat;
163
+ try {
164
+ stat = fs.statSync(candidate);
165
+ }
166
+ catch {
167
+ stat = undefined;
168
+ }
169
+ if (stat)
170
+ return toEntry(rel, stat.isDirectory());
171
+ }
172
+ }
173
+ return toEntry(namedPath.replace(/^\.\//, ""), namedPath.endsWith("/"));
174
+ }
175
+ /**
176
+ * Re-render hq-cloud's UnreachablePushPathsError with the real blocker per
177
+ * path (feedback_6afa6ce4). The upstream message always ends with advice
178
+ * about symlinks, so a path left out of `.hqinclude` read as a symlink
179
+ * problem. This names the `.hqinclude` line to add, and keeps the symlink
180
+ * advice only for reasons that are about links or paths outside the company
181
+ * folder. Returns null for any other error.
182
+ */
183
+ export function formatUnreachablePushPaths(err, ctx = {}) {
184
+ const parsed = asUnreachablePushPaths(err);
185
+ if (!parsed || parsed.paths.length === 0)
186
+ return null;
187
+ const upstreamLines = parsed.message.split("\n");
188
+ const upstreamLine = (p) => upstreamLines.find((l) => l.startsWith(" · " + p + " — "));
189
+ const ruleFor = (p) => {
190
+ const line = upstreamLine(p);
191
+ if (!line)
192
+ return undefined;
193
+ return line.slice((" · " + p + " — ").length).replace(/; it was not uploaded$/, "");
194
+ };
195
+ const includeEntries = [];
196
+ let linkAdvice = false;
197
+ const lines = parsed.paths.map((p) => {
198
+ const reason = parsed.reasons[p];
199
+ if (reason === "ignored") {
200
+ const rule = ruleFor(p) ?? "excluded by a sync ignore rule";
201
+ if (rule === "not listed in .hqinclude") {
202
+ const entry = hqincludeEntryFor(p, ctx);
203
+ includeEntries.push(entry);
204
+ return " · " + p + " — not listed in .hqinclude, so sync skips it. Add this line to .hqinclude: " + entry;
205
+ }
206
+ return " · " + p + " — " + rule + "; it was not uploaded";
207
+ }
208
+ if (reason === "outside-company" || reason === "unreadable-link") {
209
+ linkAdvice = true;
210
+ }
211
+ return upstreamLine(p) ?? " · " + p + " — could not be pushed (" + String(reason) + ")";
212
+ });
213
+ const count = parsed.paths.length;
214
+ const out = [
215
+ count + " named path" + (count === 1 ? "" : "s") + " could not be pushed; nothing was uploaded.",
216
+ ...lines,
217
+ ];
218
+ if (includeEntries.length > 0) {
219
+ out.push("Your HQ root has a .hqinclude allowlist, so only listed paths sync. Add the line" +
220
+ (includeEntries.length === 1 ? "" : "s") +
221
+ " above to .hqinclude at the HQ root, then push again.");
222
+ }
223
+ if (linkAdvice) {
224
+ out.push("A path reached through a symlink is only pushable when it stays inside the HQ tree " +
225
+ "(e.g. companies/<slug>/knowledge → repos/private/knowledge-<slug>); one that points " +
226
+ "outside HQ has to sync through whatever owns it, not the vault.");
227
+ }
228
+ return out.join("\n");
229
+ }
230
+ /**
231
+ * Lines naming the blocker when a push with user-named paths uploaded nothing
232
+ * and every not-uploaded file fell outside write scope (feedback_5baccb9e).
233
+ * The summary before this gave a count and the granted prefixes; this names
234
+ * each requested path as the thing no write grant covers and points at
235
+ * `hq access`, which reports the ACL row that blocks it.
236
+ */
237
+ export function explicitPushBlockedLines(input) {
238
+ if (!input.userNamedPaths || input.namedPaths.length === 0)
239
+ return [];
240
+ if (input.filesUploaded > 0 || input.filesExcludedByScope <= 0)
241
+ return [];
242
+ if (input.otherNotUploaded > 0)
243
+ return [];
244
+ const keys = input.namedPaths.map((p) => hqincludeEntryFor(p, {
245
+ hqRoot: input.hqRoot,
246
+ cwd: input.cwd,
247
+ companySlug: input.companySlug,
248
+ }).replace(/\/$/, ""));
249
+ const lines = [
250
+ "✗ None of the paths you named were uploaded: every file in them is outside your write access.",
251
+ ];
252
+ for (const key of keys) {
253
+ lines.push(input.prefixSet === undefined
254
+ ? " · " + key + " — the server refused writes here (an ACL on this path or a parent blocks you)"
255
+ : " · " + key + " — no write prefix you hold covers this path");
256
+ }
257
+ lines.push(" Run `hq access " + keys[0] + "` to see which ACL row blocks the write, then ask its creator or a company admin for write access.");
258
+ return lines;
259
+ }
108
260
  /**
109
261
  * US-003: run one sync fanout under a client-health attempt/outcome report.
110
262
  * The report is best-effort, bounded, and swallowed — it can only append a
@@ -298,6 +450,34 @@ function resolveDeletePolicy() {
298
450
  return "currency-gated";
299
451
  }
300
452
  const DEFAULT_SYNC_LOCK_TIMEOUT_SECONDS = 300;
453
+ /**
454
+ * feedback_b7c00a62: run a `sync now` runner under the watchdog (see
455
+ * `utils/sync-watchdog.ts` for the limits). A timeout prints a clear message
456
+ * and unwinds as a failed {@link SyncExitError}, so {@link withSyncHealthReport}
457
+ * records the failure and exits 124. The failure report gets at most
458
+ * {@link SYNC_WATCHDOG_REPORT_GRACE_MS}; after that the process exits 124
459
+ * regardless, and the worker backstop stays armed behind both.
460
+ */
461
+ export async function runUnderSyncWatchdog(limits, label, run, watchdog = withSyncWatchdog, exit = (code) => process.exit(code)) {
462
+ try {
463
+ return await watchdog(run, {
464
+ idleMs: limits.idleSeconds * 1000,
465
+ maxRuntimeMs: limits.maxRuntimeSeconds * 1000,
466
+ label,
467
+ probeProgress: createSnapshotProgressProbe(readSyncProgress),
468
+ });
469
+ }
470
+ catch (err) {
471
+ if (err instanceof SyncWatchdogTimeoutError) {
472
+ console.error(chalk.red("\n\u2717 " + err.message));
473
+ process.exitCode = SYNC_WATCHDOG_EXIT_CODE;
474
+ const forceExit = setTimeout(() => exit(SYNC_WATCHDOG_EXIT_CODE), SYNC_WATCHDOG_REPORT_GRACE_MS);
475
+ forceExit.unref();
476
+ throw new SyncExitError(SYNC_WATCHDOG_EXIT_CODE);
477
+ }
478
+ throw err;
479
+ }
480
+ }
301
481
  /**
302
482
  * Bound foreground waits for a watcher/manual sync that currently owns the
303
483
  * per-root operation lock. The cloud engine reads this environment value on
@@ -947,6 +1127,9 @@ export function registerCloudCommands(program) {
947
1127
  // failures never count as a sync attempt (US-003).
948
1128
  let syncHealth;
949
1129
  let explicitPersonalPaths;
1130
+ // Company folder the push resolves relative paths against; names the
1131
+ // .hqinclude line and the blocked path in failure output.
1132
+ let pushCompanySlug;
950
1133
  try {
951
1134
  if (options.personal && options.credsFromStdin) {
952
1135
  throw new Error("`--personal` cannot be combined with --creds-from-stdin: " +
@@ -1063,6 +1246,14 @@ export function registerCloudCommands(program) {
1063
1246
  companyRef: targetCompany ?? readActiveCompany(options.hqRoot),
1064
1247
  });
1065
1248
  const planLimitTracker = createPlanLimitRefusalTracker(companySlugForBilling(progressCompany));
1249
+ // A cmp_ uid target has no slug in progressCompany; fall back to
1250
+ // the entity slug, then the company folder the paths sit in.
1251
+ pushCompanySlug =
1252
+ companySlugForBilling(progressCompany).companySlug ??
1253
+ (entityContext && !entityContext.uid.startsWith("prs_")
1254
+ ? entityContext.slug
1255
+ : undefined) ??
1256
+ (personalMode ? undefined : companySlugFromSyncPaths(options.hqRoot, targetPaths));
1066
1257
  const identityHeld = createIdentityHeldTracker();
1067
1258
  syncHealth = beginSyncHealthReport();
1068
1259
  if (!personalMode) {
@@ -1191,6 +1382,18 @@ export function registerCloudCommands(program) {
1191
1382
  for (const line of scopeExcludedLines(result.filesExcludedByScope, pushPrefixSet)) {
1192
1383
  log(chalk.yellow(line));
1193
1384
  }
1385
+ for (const line of explicitPushBlockedLines({
1386
+ userNamedPaths,
1387
+ namedPaths: targetPaths,
1388
+ filesUploaded: result.filesUploaded,
1389
+ filesExcludedByScope: result.filesExcludedByScope,
1390
+ otherNotUploaded: staleRefusalCount + uploadFailed.count + identityHeldCount,
1391
+ prefixSet: pushPrefixSet,
1392
+ hqRoot: options.hqRoot,
1393
+ companySlug: pushCompanySlug,
1394
+ })) {
1395
+ log(chalk.red(line));
1396
+ }
1194
1397
  for (const line of staleUploadRefusalLines(staleRefusalCount, result.filesRefusedStalePaths)) {
1195
1398
  log(chalk.yellow(line));
1196
1399
  }
@@ -1220,7 +1423,10 @@ export function registerCloudCommands(program) {
1220
1423
  }
1221
1424
  if (syncHealth)
1222
1425
  await syncHealth.failed();
1223
- const message = formatSyncFailure(err);
1426
+ const message = formatSyncFailure(err, {
1427
+ hqRoot: options.hqRoot,
1428
+ companySlug: pushCompanySlug,
1429
+ });
1224
1430
  if (jsonMode) {
1225
1431
  // In JSON mode, the parent process is parsing stderr for ndjson —
1226
1432
  // human-formatted error lines would corrupt the stream. Emit a
@@ -1509,6 +1715,14 @@ export function registerCloudCommands(program) {
1509
1715
  .option("--force-scope-shrink", "When a scope shrink would un-track locally-modified files, proceed " +
1510
1716
  "anyway. Dirty files are KEPT on disk (only un-tracked); clean " +
1511
1717
  "out-of-scope files are quarantined under .hq/scope-quarantine/.")
1718
+ .option("--max-idle <seconds>", "For scheduled or unattended runs: stop with exit code " + SYNC_WATCHDOG_EXIT_CODE +
1719
+ " after this many seconds with no file planned, transferred or skipped. The remote " +
1720
+ "listing and planning phase of a leg reports no progress, so set this longer than " +
1721
+ "your slowest planning phase. Laptop sleep does not count. " +
1722
+ "(default: HQ_SYNC_MAX_IDLE, else off)")
1723
+ .option("--max-runtime <seconds>", "For scheduled or unattended runs: stop with exit code " + SYNC_WATCHDOG_EXIT_CODE +
1724
+ " if the run has not finished after this many seconds, even while it is making progress. " +
1725
+ "Laptop sleep does not count. (default: HQ_SYNC_MAX_RUNTIME, else off)")
1512
1726
  .action(async (options) => {
1513
1727
  const skips = refuseSyncForOffVaults("now", {
1514
1728
  all: options.all,
@@ -1519,18 +1733,22 @@ export function registerCloudCommands(program) {
1519
1733
  try {
1520
1734
  configureSyncLockTimeout(options.lockTimeout);
1521
1735
  assertSingleSelector(options, "now");
1736
+ const watchdogLimits = resolveSyncWatchdogLimits({
1737
+ maxIdle: options.maxIdle,
1738
+ maxRuntime: options.maxRuntime,
1739
+ });
1522
1740
  if (options.all) {
1523
1741
  // `options.personal === false` is Commander's auto-negation
1524
1742
  // of `--personal`; in `--all` mode that means "drop the
1525
1743
  // personal leg from both legs of the bidirectional fanout"
1526
1744
  // (see `--no-personal` option above).
1527
- await withSyncHealthReport(() => runNowAll(options.hqRoot, options.message, options.onConflict, options.modeAll === true, skips.skipPersonal, options.forceScopeShrink === true, skips.skipCompanies));
1745
+ await withSyncHealthReport(() => runUnderSyncWatchdog(watchdogLimits, "hq sync now --all", () => runNowAll(options.hqRoot, options.message, options.onConflict, options.modeAll === true, skips.skipPersonal, options.forceScopeShrink === true, skips.skipCompanies)));
1528
1746
  return;
1529
1747
  }
1530
- await withSyncHealthReport(() => runNowSingle(options.hqRoot, options.company, options.personal === true, options.message, options.onConflict, options.modeAll === true, options.forceScopeShrink === true));
1748
+ await withSyncHealthReport(() => runUnderSyncWatchdog(watchdogLimits, "hq sync now", () => runNowSingle(options.hqRoot, options.company, options.personal === true, options.message, options.onConflict, options.modeAll === true, options.forceScopeShrink === true)));
1531
1749
  }
1532
1750
  catch (err) {
1533
- console.error(chalk.red("\n✗ Sync now failed:"), err instanceof Error ? err.message : String(err));
1751
+ console.error(chalk.red("\n✗ Sync now failed:"), describeSyncError(err));
1534
1752
  process.exit(1);
1535
1753
  }
1536
1754
  });
@@ -1680,7 +1898,7 @@ async function runPullAll(hqRoot, onConflict, modeAllOverride, skipPersonal, for
1680
1898
  });
1681
1899
  }
1682
1900
  catch (err) {
1683
- console.error(chalk.red("\n✗ Pull-all failed:"), err instanceof Error ? err.message : String(err));
1901
+ console.error(chalk.red("\n✗ Pull-all failed:"), describeSyncError(err));
1684
1902
  // Unwind (never process.exit) so withSyncHealthReport records the
1685
1903
  // terminal sync_failure before exiting on our behalf.
1686
1904
  throw new SyncExitError(1);
@@ -1749,7 +1967,7 @@ export async function runPullPersonal(hqRoot, onConflict) {
1749
1967
  catch (err) {
1750
1968
  if (err instanceof SyncExitError)
1751
1969
  throw err;
1752
- console.error(chalk.red("\n✗ Pull (personal) failed:"), err instanceof Error ? err.message : String(err));
1970
+ console.error(chalk.red("\n✗ Pull (personal) failed:"), describeSyncError(err));
1753
1971
  throw new SyncExitError(1);
1754
1972
  }
1755
1973
  }
@@ -1834,7 +2052,7 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
1834
2052
  });
1835
2053
  }
1836
2054
  catch (err) {
1837
- console.error(chalk.red("\n✗ Push-all failed:"), err instanceof Error ? err.message : String(err));
2055
+ console.error(chalk.red("\n✗ Push-all failed:"), describeSyncError(err));
1838
2056
  // Unwind (never process.exit) so withSyncHealthReport records the
1839
2057
  // terminal sync_failure before exiting on our behalf.
1840
2058
  throw new SyncExitError(1);
@@ -2185,7 +2403,7 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2185
2403
  catch (err) {
2186
2404
  if (err instanceof SyncExitError)
2187
2405
  throw err;
2188
- console.error(chalk.red("\n✗ Sync now failed:"), err instanceof Error ? err.message : String(err));
2406
+ console.error(chalk.red("\n✗ Sync now failed:"), describeSyncError(err));
2189
2407
  throw new SyncExitError(1);
2190
2408
  }
2191
2409
  }
@@ -1,3 +1,10 @@
1
- export declare const CORE_COMPANY_INDEX_SCOPE_FLAG = "cli.rebuild-index-company-scope";
1
+ import { CORE_COMPANY_INDEX_SCOPE_FLAG } from "../lib/cli-flag-keys.js";
2
+ export { CORE_COMPANY_INDEX_SCOPE_FLAG };
3
+ /**
4
+ * Kill switch for company-scoped index rebuilds, default ON
5
+ * (indigo-feature-flags-default-on-as-fail-safes). An absent, archived or
6
+ * unreadable flag row keeps scoping on; only an explicit false turns it off,
7
+ * and then `companies` rebuilds every company as it did before scoping.
8
+ */
2
9
  export declare function coreCompanyIndexScopeEnabled(): boolean;
3
10
  //# sourceMappingURL=core-company-index-scope-flag.d.ts.map
@@ -1,6 +1,13 @@
1
1
  import { resolveProcessFlagGate } from "../lib/flag-registry.js";
2
- export const CORE_COMPANY_INDEX_SCOPE_FLAG = "cli.rebuild-index-company-scope";
2
+ import { CORE_COMPANY_INDEX_SCOPE_FLAG } from "../lib/cli-flag-keys.js";
3
+ export { CORE_COMPANY_INDEX_SCOPE_FLAG };
4
+ /**
5
+ * Kill switch for company-scoped index rebuilds, default ON
6
+ * (indigo-feature-flags-default-on-as-fail-safes). An absent, archived or
7
+ * unreadable flag row keeps scoping on; only an explicit false turns it off,
8
+ * and then `companies` rebuilds every company as it did before scoping.
9
+ */
3
10
  export function coreCompanyIndexScopeEnabled() {
4
- return resolveProcessFlagGate(CORE_COMPANY_INDEX_SCOPE_FLAG, { fallback: false }, () => false);
11
+ return resolveProcessFlagGate(CORE_COMPANY_INDEX_SCOPE_FLAG, { fallback: true }, () => true, undefined, { acceptStaleSnapshot: true, killSwitchWithoutCompany: true });
5
12
  }
6
13
  //# sourceMappingURL=core-company-index-scope-flag.js.map
@@ -3,6 +3,7 @@ import { createHash, randomInt } from "node:crypto";
3
3
  import * as fs from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
+ import { callerIsMachinePrincipal } from "../lib/caller-principal.js";
6
7
  import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
7
8
  import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
8
9
  import { matchingSecretPatterns } from "../lib/core-utils/secret-patterns.js";
@@ -1079,8 +1080,10 @@ function runSend(args, context) {
1079
1080
  catch {
1080
1081
  return fail("send", "could not persist sending state; no DM attempted");
1081
1082
  }
1082
- // The pickup DM is read by the person and acted on by their agent.
1083
- const dm = runHq(["dm", principal, headline, "--audience", "both", "--prompt-file", prompt, "--details-file", brief], context);
1083
+ // The pickup DM is read by the person and acted on by their agent. Only an
1084
+ // agent or bot may tag it `both`; a person login sends `human`.
1085
+ const audience = callerIsMachinePrincipal() ? "both" : "human";
1086
+ const dm = runHq(["dm", principal, headline, "--audience", audience, "--prompt-file", prompt, "--details-file", brief], context);
1084
1087
  if (dm.code !== 0)
1085
1088
  return fail("send", "hq dm delivery outcome is uncertain; status is sending, inspect DM history before retrying");
1086
1089
  const event = /eventId[: ]+([A-Za-z0-9_-]+)/.exec(dm.stdout ?? "")?.[1] ?? "";
@@ -2,6 +2,7 @@
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
+ import { tokenIsMachinePrincipal } from "../lib/caller-principal.js";
5
6
  import { spawnSync } from "node:child_process";
6
7
  import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
7
8
  import { resolveLiveRoot } from "../utils/hq-roots.js";
@@ -329,9 +330,12 @@ function latestCliEventId(d, ch) {
329
330
  });
330
331
  return String(jqAlternative(rows.at(-1)?.eventId, jqAlternative(rows.at(-1)?.messageId, "")));
331
332
  }
332
- async function sendHuman(d, ch, payload) {
333
+ /** The ID token every human-API request in this file sends as its bearer. */
334
+ function humanApiToken(d) {
335
+ return JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/cognito-tokens.json"), "utf8")).idToken;
336
+ }
337
+ async function sendHuman(d, ch, payload, token = humanApiToken(d)) {
333
338
  const config = JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/config.json"), "utf8"));
334
- const token = JSON.parse(d.fs.readFileSync(path.join(d.home, ".hq/cognito-tokens.json"), "utf8")).idToken;
335
339
  const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
336
340
  const channelsResponse = await d.fetch(`${config.vaultApiUrl}/v1/notify/channels`, { headers, signal: AbortSignal.timeout(DM_BIND_API_TIMEOUT_MS) });
337
341
  const channels = await channelsResponse.json();
@@ -596,11 +600,22 @@ async function post(d, args) {
596
600
  const mentions = mentionsJson(d, sid, names);
597
601
  if (!mentions.length)
598
602
  return fail("no mention resolved to a channel member id — not posting an update that notifies nobody");
599
- const payload = { body: postedMessage, mentions, audience: "agent" };
603
+ // People never send agent-tagged messages: a person login posts `human`.
604
+ // The audience is read from the token this request sends as its bearer, so
605
+ // a box with machine creds plus a person cache cannot tag a person-token
606
+ // post `agent` (the server rejects that with 403).
607
+ let token;
608
+ try {
609
+ token = humanApiToken(d);
610
+ }
611
+ catch (error) {
612
+ return utilityError(`hq-dm-bind: ${error.message}\n`, 1);
613
+ }
614
+ const payload = { body: postedMessage, mentions, audience: tokenIsMachinePrincipal(token) ? "agent" : "human" };
600
615
  if (root)
601
616
  payload.rootEventId = root;
602
617
  try {
603
- eventId = await sendHuman(d, ch, payload);
618
+ eventId = await sendHuman(d, ch, payload, token);
604
619
  }
605
620
  catch (error) {
606
621
  return utilityError(`hq-dm-bind: ${error.message}\n`, 1);
@@ -1,5 +1,7 @@
1
1
  /** Isolated fast-entry adapter for rebuild-index native renderers. */
2
2
  import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
3
+ import { planScopedRebuild } from "../lib/index-render/session-scope.js";
4
+ import { CORE_COMPANY_INDEX_SCOPE_FLAG } from "../lib/cli-flag-keys.js";
3
5
  const targets = new Set([
4
6
  "all", "companies", "company-knowledge", "orchestrator", "projects",
5
7
  "public-knowledge", "reports", "social-drafts", "threads", "workers",
@@ -10,7 +12,10 @@ const targets = new Set([
10
12
  */
11
13
  export async function runRebuildIndexFast(args, _stdin, _cwd, hqRoot) {
12
14
  const [target, ...rest] = args;
13
- if (!target || !targets.has(target) || rest.length > 0 || !hqRoot)
15
+ // The only argument the fast route takes is --all-companies; anything else
16
+ // needs the full graph's argument handling.
17
+ const allCompanies = rest.length === 1 && rest[0] === "--all-companies";
18
+ if (!target || !targets.has(target) || (rest.length > 0 && !allCompanies) || !hqRoot)
14
19
  return undefined;
15
20
  const logs = [];
16
21
  const flushLogs = () => {
@@ -19,11 +24,24 @@ export async function runRebuildIndexFast(args, _stdin, _cwd, hqRoot) {
19
24
  };
20
25
  try {
21
26
  const refused = [];
22
- renderIndexTarget(target, {
23
- root: hqRoot,
24
- log: (message) => logs.push(message),
25
- refused,
26
- });
27
+ // Same session company scope as the full command, resolved before any
28
+ // write. A refusal falls back to the full graph, which reports it.
29
+ // The full flag registry is off-limits on the fast route; read the
30
+ // default-on kill switch from the lightweight snapshot reader, and only for
31
+ // the targets that need it.
32
+ const companyIndexScopeEnabled = target === "companies" || target === "all"
33
+ ? await (await import("../native-fast-route.js")).fastFlagEnabledDefaultOn(CORE_COMPANY_INDEX_SCOPE_FLAG)
34
+ : true;
35
+ const plan = planScopedRebuild(target, rest, hqRoot, process.env, { companyIndexScopeEnabled });
36
+ if (plan.warning)
37
+ logs.push(plan.warning);
38
+ for (const run of plan.runs) {
39
+ renderIndexTarget(run.target, {
40
+ root: hqRoot,
41
+ log: (message) => logs.push(message),
42
+ refused,
43
+ }, run.args);
44
+ }
27
45
  if (refused.length > 0)
28
46
  return undefined;
29
47
  flushLogs();
@@ -57,6 +57,7 @@ import { registerCoreWorkflowRunCommand } from "./core-workflow-run-command.js";
57
57
  import { resolveLiveRoot } from "../utils/hq-roots.js";
58
58
  import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
59
59
  import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
60
+ import { planScopedRebuild, SESSION_SCOPE_HELP } from "../lib/index-render/session-scope.js";
60
61
  import { generateWorkersRegistry } from "../lib/workers-registry/index.js";
61
62
  import { scanPackages } from "../lib/scan-packages/index.js";
62
63
  import { detectStaleCorePolicyMirror } from "../lib/core-utils/detect-stale-core-policy-mirror.js";
@@ -671,6 +672,7 @@ export function registerCoreCommands(program) {
671
672
  const wantsHelp = rendererArgs.some((arg) => arg === "--help" || arg === "-h");
672
673
  if (wantsHelp && ["companies", "company-knowledge", "projects"].includes(entry.renderer)) {
673
674
  renderIndexTarget(entry.renderer, { root: "", output: (message) => process.stdout.write(message) }, rendererArgs);
675
+ process.stdout.write(SESSION_SCOPE_HELP);
674
676
  return;
675
677
  }
676
678
  if (wantsHelp) {
@@ -686,12 +688,22 @@ export function registerCoreCommands(program) {
686
688
  const hqRoot = resolveLiveRoot({ hqRoot: scope.hqRoot });
687
689
  const refused = [];
688
690
  try {
689
- renderIndexTarget(entry.renderer, {
690
- root: hqRoot,
691
- log: (message) => process.stderr.write(`${message}\n`),
692
- force,
693
- refused,
694
- }, rendererArgs);
691
+ // Resolve the session's company scope before any write: a bound
692
+ // session rebuilds only its bound companies, and an unbound one must
693
+ // name a company or pass --all-companies.
694
+ const plan = planScopedRebuild(entry.renderer, rendererArgs, hqRoot, process.env, {
695
+ companyIndexScopeEnabled: coreCompanyIndexScopeEnabled(),
696
+ });
697
+ if (plan.warning)
698
+ process.stderr.write(`${plan.warning}\n`);
699
+ for (const run of plan.runs) {
700
+ renderIndexTarget(run.target, {
701
+ root: hqRoot,
702
+ log: (message) => process.stderr.write(`${message}\n`),
703
+ force,
704
+ refused,
705
+ }, run.args);
706
+ }
695
707
  }
696
708
  catch (error) {
697
709
  if (error instanceof CompanyIndexUserError)
@@ -426,7 +426,7 @@ function formatUnit(u) {
426
426
  ? `last run ${u.lastResult}`
427
427
  : (u.reason ?? "");
428
428
  const restarts = u.restarts > 0 ? ` (restarted ${u.restarts}x)` : "";
429
- return ` ${u.id.padEnd(22)} ${u.status.padEnd(12)} ${detail}${restarts}`;
429
+ return ` ${u.id.padEnd(40)} ${u.status.padEnd(12)} ${detail}${restarts}`;
430
430
  }
431
431
  async function runStatus(d, opts) {
432
432
  const host = currentHost(d);
@@ -1538,6 +1538,21 @@ export function registerDaemonCommand(program, over = {}) {
1538
1538
  .command("self-update")
1539
1539
  .description("Update the hq CLI, Claude, Codex and Grok, then restart the daemon onto a new CLI")
1540
1540
  .action(wrap((d) => runServiceSelfUpdate(d)));
1541
+ service
1542
+ .command("sync-core-reconcile")
1543
+ .description("Read each company's new-sync-engine switch and write this machine's engine homes and legacy off-switches")
1544
+ .option("--hq-root <path>", "HQ folder the engine works against")
1545
+ .action(wrap(async (d, opts) => {
1546
+ const { runSyncCoreReconcile } = await import("./sync-core.js");
1547
+ await runSyncCoreReconcile({ hqRoot: opts.hqRoot }, {
1548
+ home: d.home,
1549
+ env: d.env,
1550
+ platform: d.platform,
1551
+ arch: process.arch,
1552
+ out: d.out,
1553
+ err: d.err,
1554
+ });
1555
+ }));
1541
1556
  service
1542
1557
  .command("remote-control")
1543
1558
  .description("Keep `claude remote-control` running in a tmux session in the HQ folder")
@@ -77,6 +77,14 @@ export interface DmContact {
77
77
  * eba06f59). Returns null when nothing or more than one contact matches. Pure.
78
78
  */
79
79
  export declare function matchContactByName(contacts: DmContact[], name: string): DmContact | null;
80
+ /**
81
+ * Every distinct contact (person or bot) whose display name matches `name`,
82
+ * using the same rule as matchContactByName. A leading '@' is the explicit
83
+ * person/bot selector and is ignored for matching. Pure.
84
+ */
85
+ export declare function matchContactsByName(contacts: DmContact[], name: string): DmContact[];
86
+ /** One line per ambiguous contact match, for a refusal message. Pure. */
87
+ export declare function formatContactMatches(matches: DmContact[]): string;
80
88
  /**
81
89
  * The confirmation line for a channel send. When the typed target is not the
82
90
  * channel's own display name (for example `hq dm setup` matching a channel
@@ -236,15 +244,21 @@ export declare function fetchChannels(token: string): Promise<ChannelSummary[]>;
236
244
  * post itself passes. Used to resolve `@name` mentions into participant uids.
237
245
  */
238
246
  export declare function fetchChannelMembers(token: string, channelId: string): Promise<ChannelMember[]>;
247
+ /** An explicit agent/both audience from a person login. */
248
+ export declare class PersonAudienceError extends Error {
249
+ constructor();
250
+ }
239
251
  /**
240
252
  * Resolve the audience for a send. The default comes from the recipient kind
241
- * (`defaultAudienceFor`); `--for-human` maps to `human`; an explicit
242
- * `--audience` wins over `--for-human` (with a warning) and is validated.
253
+ * and the caller (`defaultAudienceFor`: always `human` for a person login);
254
+ * `--for-human` maps to `human`; an explicit `--audience` wins over
255
+ * `--for-human` (with a warning) and is validated. A person login passing
256
+ * `--audience agent|both` throws PersonAudienceError.
243
257
  */
244
258
  export declare function resolveSendAudience(opts: {
245
259
  audience?: string;
246
260
  forHuman?: boolean;
247
- }, recipientKind: DmRecipientKind, warn?: (msg: string) => void): string;
261
+ }, recipientKind: DmRecipientKind, warn?: (msg: string) => void, machineCaller?: boolean): string;
248
262
  /** One incoming DM as returned by GET /v1/notify/inbox. */
249
263
  export interface DmInboxEvent {
250
264
  eventId: string;