@indigoai-us/hq-cli 5.345.156 → 5.345.159

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 (144) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
  3. package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
  4. package/assets/scaffold/core/scripts/worktree.sh +4 -0
  5. package/dist/command-catalog.generated.d.ts +531 -6
  6. package/dist/command-catalog.generated.js +674 -27
  7. package/dist/command-registration-plan.d.ts +18 -0
  8. package/dist/command-registration-plan.js +4 -0
  9. package/dist/commands/_patterns.d.ts +17 -0
  10. package/dist/commands/_patterns.js +22 -0
  11. package/dist/commands/agent-kit.js +8 -6
  12. package/dist/commands/bot.js +7 -4
  13. package/dist/commands/channel-suggest.d.ts +56 -0
  14. package/dist/commands/channel-suggest.js +170 -0
  15. package/dist/commands/channels.js +7 -1
  16. package/dist/commands/cloud.d.ts +45 -0
  17. package/dist/commands/cloud.js +150 -10
  18. package/dist/commands/company.js +4 -3
  19. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  20. package/dist/commands/core-checkpoint-native.js +67 -2
  21. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  22. package/dist/commands/core-pipeline-driver-native.js +131 -5
  23. package/dist/commands/core.js +12 -0
  24. package/dist/commands/crm.js +83 -6
  25. package/dist/commands/deploy-access.d.ts +33 -0
  26. package/dist/commands/deploy-access.js +322 -0
  27. package/dist/commands/dm-attach.d.ts +106 -0
  28. package/dist/commands/dm-attach.js +184 -0
  29. package/dist/commands/dm.d.ts +37 -0
  30. package/dist/commands/dm.js +172 -10
  31. package/dist/commands/feedback-show.d.ts +40 -0
  32. package/dist/commands/feedback-show.js +261 -0
  33. package/dist/commands/feedback.d.ts +21 -0
  34. package/dist/commands/feedback.js +152 -2
  35. package/dist/commands/files.d.ts +24 -0
  36. package/dist/commands/files.js +52 -1
  37. package/dist/commands/integrations-connect.js +7 -3
  38. package/dist/commands/lanes-accounts.js +16 -1
  39. package/dist/commands/lanes-loop.js +14 -4
  40. package/dist/commands/lanes-message.js +6 -4
  41. package/dist/commands/lanes-resume.js +36 -13
  42. package/dist/commands/lanes-shared.d.ts +5 -0
  43. package/dist/commands/lanes-shared.js +13 -3
  44. package/dist/commands/lanes-watch.js +1 -0
  45. package/dist/commands/lanes.d.ts +20 -1
  46. package/dist/commands/lanes.js +201 -51
  47. package/dist/commands/mesh.js +149 -1
  48. package/dist/commands/monitor.d.ts +1 -0
  49. package/dist/commands/monitor.js +4 -1
  50. package/dist/commands/repo.d.ts +41 -0
  51. package/dist/commands/repo.js +331 -0
  52. package/dist/commands/scan.d.ts +3 -1
  53. package/dist/commands/scan.js +7 -3
  54. package/dist/commands/secrets.d.ts +21 -0
  55. package/dist/commands/secrets.js +114 -14
  56. package/dist/commands/signals.js +17 -5
  57. package/dist/commands/staff/listings.d.ts +41 -0
  58. package/dist/commands/staff/listings.js +160 -0
  59. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  60. package/dist/commands/staff/reset-test-account.js +4 -3
  61. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
  62. package/dist/hq-cloud-sync-runner/package.json +1 -1
  63. package/dist/lib/agent-kit/run/inbox.js +2 -1
  64. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  65. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  66. package/dist/lib/agent-kit/wake.d.ts +4 -0
  67. package/dist/lib/agent-kit/wake.js +49 -3
  68. package/dist/lib/bot/inflight.d.ts +7 -1
  69. package/dist/lib/bot/inflight.js +10 -2
  70. package/dist/lib/bot/prompt.js +3 -0
  71. package/dist/lib/bot/run.js +72 -20
  72. package/dist/lib/bot/session.d.ts +14 -0
  73. package/dist/lib/bot/session.js +70 -0
  74. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  75. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  76. package/dist/lib/capacity/lane-login.d.ts +6 -0
  77. package/dist/lib/capacity/lane-login.js +5 -0
  78. package/dist/lib/core-utils/worktree.js +2 -0
  79. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  80. package/dist/lib/daemon/exit-cause.js +88 -0
  81. package/dist/lib/daemon/handoff.d.ts +2 -0
  82. package/dist/lib/daemon/handoff.js +6 -1
  83. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  84. package/dist/lib/daemon/paths.d.ts +2 -0
  85. package/dist/lib/daemon/paths.js +1 -0
  86. package/dist/lib/daemon/run.js +13 -0
  87. package/dist/lib/daemon/supervisor.d.ts +54 -1
  88. package/dist/lib/daemon/supervisor.js +297 -21
  89. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  90. package/dist/lib/daemon/unit-holds.js +86 -0
  91. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  92. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  93. package/dist/lib/index-render/company-knowledge.js +1 -3
  94. package/dist/lib/index-render/projects.js +1 -3
  95. package/dist/lib/lanes/account-company.d.ts +28 -0
  96. package/dist/lib/lanes/account-company.js +53 -0
  97. package/dist/lib/lanes/accounts.d.ts +22 -3
  98. package/dist/lib/lanes/accounts.js +38 -2
  99. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  100. package/dist/lib/lanes/claude-balance.js +6 -1
  101. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  102. package/dist/lib/lanes/claude-credential.js +163 -0
  103. package/dist/lib/lanes/compaction.d.ts +2 -0
  104. package/dist/lib/lanes/compaction.js +9 -0
  105. package/dist/lib/lanes/config.js +6 -0
  106. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  107. package/dist/lib/lanes/hooks-install.js +2 -1
  108. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  109. package/dist/lib/lanes/host-capacity.js +1 -0
  110. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  111. package/dist/lib/lanes/keep-alive.js +3 -1
  112. package/dist/lib/lanes/links.js +10 -4
  113. package/dist/lib/lanes/loop.js +16 -1
  114. package/dist/lib/lanes/node-binary.d.ts +17 -0
  115. package/dist/lib/lanes/node-binary.js +30 -0
  116. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  117. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  118. package/dist/lib/lanes/reconcile.js +4 -0
  119. package/dist/lib/lanes/remote-control-lane.js +2 -0
  120. package/dist/lib/lanes/spawn.d.ts +3 -0
  121. package/dist/lib/lanes/spawn.js +119 -9
  122. package/dist/lib/lanes/store.d.ts +6 -0
  123. package/dist/lib/lanes/store.js +107 -18
  124. package/dist/lib/lanes/types.d.ts +2 -0
  125. package/dist/lib/lanes/watch.js +28 -5
  126. package/dist/lib/lanes/watcher-main.js +4 -3
  127. package/dist/lib/lanes/watcher.d.ts +4 -0
  128. package/dist/lib/lanes/watcher.js +110 -21
  129. package/dist/lib/mesh/api.d.ts +32 -0
  130. package/dist/lib/mesh/api.js +75 -0
  131. package/dist/lib/secret-scan/detect.js +9 -0
  132. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  133. package/dist/lib/secret-scan/repo-config.js +237 -0
  134. package/dist/lib/secret-scan/rules.d.ts +7 -1
  135. package/dist/lib/secret-scan/rules.js +3 -1
  136. package/dist/lib/secret-scan/staged.d.ts +5 -0
  137. package/dist/lib/secret-scan/staged.js +19 -2
  138. package/dist/lib/secret-scan/types.d.ts +2 -0
  139. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  140. package/dist/lib/sync/server-owned-local-only.js +75 -0
  141. package/dist/utils/package-use-lease.js +27 -9
  142. package/dist/utils/version-gate.d.ts +7 -0
  143. package/dist/utils/version-gate.js +66 -0
  144. package/package.json +8 -8
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import { isAccessDeniedError, accessDeniedKeyOf, formatAccessDenied, } from "../utils/access-denied-hint.js";
16
16
  import { findReadOnlyLocalOnly, isGrantEnforced, readVaultAccessManifest, } from "../lib/sync/read-only-local-only.js";
17
+ import { SERVER_OWNED_FOLDERS, findServerOwnedLocalOnly, } from "../lib/sync/server-owned-local-only.js";
17
18
  import chalk from "chalk";
18
19
  import * as fs from "fs";
19
20
  import * as path from "path";
@@ -165,6 +166,37 @@ export function staleUploadRefusalLines(count, paths = []) {
165
166
  }
166
167
  return lines;
167
168
  }
169
+ export function readUploadFailures(result) {
170
+ const r = result;
171
+ return { count: r.filesUploadFailed ?? 0, paths: r.uploadFailedPaths ?? [] };
172
+ }
173
+ /**
174
+ * Files the vault accepted in the plan but refused or failed at upload
175
+ * (feedback_943456f4). Without these lines a push that uploaded nothing
176
+ * printed "✓ Pushed 0" and exited 0.
177
+ */
178
+ export function uploadFailedLines(count, paths = []) {
179
+ if (count <= 0)
180
+ return [];
181
+ const lines = [
182
+ `⚠ ${count} file(s) were NOT uploaded because the upload failed. ` +
183
+ `They are still on this machine. Push again to retry them.`,
184
+ ];
185
+ if (paths.length > 0) {
186
+ lines.push(" Not uploaded:", ...paths.map((filePath) => ` - ${filePath}`));
187
+ }
188
+ return lines;
189
+ }
190
+ /**
191
+ * share() option that turns a user-named path an ignore rule drops into an
192
+ * "unreachable path" error instead of a silent 0-file success
193
+ * (feedback_63bfc302, feedback_8c294805). Spread as a plain record so this
194
+ * builds against an hq-cloud release whose ShareOptions predates the field;
195
+ * older releases ignore it.
196
+ */
197
+ export function namedPathShareOptions(userNamedPaths) {
198
+ return userNamedPaths ? { failOnIgnoredNamedPaths: true } : {};
199
+ }
168
200
  /**
169
201
  * Optional detail line listing the effective write prefixes when a push
170
202
  * dropped files — helps debug ACL vs scope-excluded mismatches (feedback_7e9c535e).
@@ -561,6 +593,12 @@ export async function pushAll(options, deps) {
561
593
  result.filesRefusedStalePaths ??= [];
562
594
  result.filesRefusedStalePaths.push(...(r.filesRefusedStalePaths ?? []));
563
595
  }
596
+ if ((r.filesUploadFailed ?? 0) > 0) {
597
+ result.filesUploadFailed =
598
+ (result.filesUploadFailed ?? 0) + (r.filesUploadFailed ?? 0);
599
+ result.uploadFailedPaths ??= [];
600
+ result.uploadFailedPaths.push(...(r.uploadFailedPaths ?? []));
601
+ }
564
602
  result.perCompany.push({
565
603
  slug: entry.slug,
566
604
  result: r,
@@ -947,6 +985,9 @@ export function registerCloudCommands(program) {
947
985
  // Otherwise preserve the historical "default to cwd" semantics so
948
986
  // `hq sync push <file>` and bare `hq sync push` from inside the
949
987
  // company tree both work as before.
988
+ // Paths the user typed fail loudly when an ignore rule drops them
989
+ // (feedback_63bfc302); enumerated defaults keep the quiet skip.
990
+ const userNamedPaths = explicitPersonalPaths !== undefined || (paths?.length ?? 0) > 0;
950
991
  const targetPaths = explicitPersonalPaths
951
992
  ? explicitPersonalPaths
952
993
  : paths && paths.length > 0
@@ -1019,9 +1060,11 @@ export function registerCloudCommands(program) {
1019
1060
  ...(journalSlug !== undefined ? { journalSlug } : {}),
1020
1061
  ...(author ? { author } : {}),
1021
1062
  ...(pushPrefixSet !== undefined ? { prefixSet: pushPrefixSet } : {}),
1063
+ ...namedPathShareOptions(userNamedPaths),
1022
1064
  });
1023
1065
  const planLimitRefused = planLimitTracker.summary();
1024
1066
  const staleRefusalCount = result.filesRefusedStale ?? 0;
1067
+ const uploadFailed = readUploadFailures(result);
1025
1068
  const refusalRecovery = [
1026
1069
  result.filesExcludedByScope > 0
1027
1070
  ? "Check write access for the refused path; ask its creator or a company admin to grant access."
@@ -1029,6 +1072,9 @@ export function registerCloudCommands(program) {
1029
1072
  staleRefusalCount > 0
1030
1073
  ? "Pull the current vault copy, merge your edit, and push again."
1031
1074
  : null,
1075
+ uploadFailed.count > 0
1076
+ ? "Push again; the files that failed are still on this machine."
1077
+ : null,
1032
1078
  ]
1033
1079
  .filter((message) => message !== null)
1034
1080
  .join(" ");
@@ -1049,6 +1095,8 @@ export function registerCloudCommands(program) {
1049
1095
  filesExcludedByScope: result.filesExcludedByScope,
1050
1096
  filesRefusedStale: staleRefusalCount,
1051
1097
  filesRefusedStalePaths: result.filesRefusedStalePaths ?? [],
1098
+ filesUploadFailed: uploadFailed.count,
1099
+ uploadFailedPaths: uploadFailed.paths,
1052
1100
  ...(refusalRecovery ? { refusalRecovery } : {}),
1053
1101
  ...(planLimitRefused ? { planLimitRefused } : {}),
1054
1102
  });
@@ -1081,26 +1129,33 @@ export function registerCloudCommands(program) {
1081
1129
  for (const line of staleUploadRefusalLines(result.filesRefusedStale ?? 0, result.filesRefusedStalePaths)) {
1082
1130
  log(chalk.yellow(line));
1083
1131
  }
1132
+ for (const line of uploadFailedLines(uploadFailed.count, uploadFailed.paths)) {
1133
+ log(chalk.yellow(line));
1134
+ }
1084
1135
  await syncHealth.succeeded();
1085
1136
  process.exit(1);
1086
1137
  return;
1087
1138
  }
1088
- if (result.filesExcludedByScope > 0 || staleRefusalCount > 0) {
1139
+ const notUploadedCount = result.filesExcludedByScope + staleRefusalCount + uploadFailed.count;
1140
+ if (notUploadedCount > 0) {
1089
1141
  log(chalk.yellow(`\n⚠ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ` +
1090
- `${result.filesSkipped} skipped, ${result.filesExcludedByScope + staleRefusalCount} files not uploaded)`));
1142
+ `${result.filesSkipped} skipped, ${notUploadedCount} files not uploaded)`));
1091
1143
  for (const line of scopeExcludedLines(result.filesExcludedByScope, pushPrefixSet)) {
1092
1144
  log(chalk.yellow(line));
1093
1145
  }
1094
1146
  for (const line of staleUploadRefusalLines(staleRefusalCount, result.filesRefusedStalePaths)) {
1095
1147
  log(chalk.yellow(line));
1096
1148
  }
1149
+ for (const line of uploadFailedLines(uploadFailed.count, uploadFailed.paths)) {
1150
+ log(chalk.yellow(line));
1151
+ }
1097
1152
  }
1098
1153
  else {
1099
1154
  const skipSummary = formatPushSkipSummary(result);
1100
1155
  log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${skipSummary})`));
1101
1156
  }
1102
1157
  await syncHealth.succeeded();
1103
- const exitCode = scopeExcludedExitCode(result.filesExcludedByScope + staleRefusalCount);
1158
+ const exitCode = scopeExcludedExitCode(notUploadedCount);
1104
1159
  if (exitCode !== 0) {
1105
1160
  process.exit(exitCode);
1106
1161
  return;
@@ -1278,6 +1333,7 @@ export function registerCloudCommands(program) {
1278
1333
  .command("status")
1279
1334
  .description("Show local sync journal summary")
1280
1335
  .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
1336
+ .option("--company <slug>", "Show only this company's journal (company slug)")
1281
1337
  .action((options, command) => {
1282
1338
  const explicitHqRoot = command.getOptionValueSource("hqRoot") === "cli";
1283
1339
  try {
@@ -1312,7 +1368,17 @@ export function registerCloudCommands(program) {
1312
1368
  // Journals live in the machine-wide state directory, independently
1313
1369
  // of the HQ content root selected for this status display.
1314
1370
  const stateDir = getStateDir();
1315
- const journals = listJournals();
1371
+ const companyFilter = options.company?.trim();
1372
+ if (options.company !== undefined && !companyFilter) {
1373
+ throw new Error("--company needs a company slug");
1374
+ }
1375
+ // Journals are keyed by slug; a company UID would silently match none.
1376
+ if (companyFilter?.startsWith("cmp_")) {
1377
+ throw new Error(`--company takes the company slug, not its UID (${companyFilter})`);
1378
+ }
1379
+ const journals = companyFilter
1380
+ ? listJournals().filter((j) => j.slug === companyFilter)
1381
+ : listJournals();
1316
1382
  let grandFiles = 0;
1317
1383
  let grandBytes = 0;
1318
1384
  let grandLastSync = "";
@@ -1320,7 +1386,13 @@ export function registerCloudCommands(program) {
1320
1386
  console.log(` HQ root: ${hqRoot}`);
1321
1387
  console.log(` Active company: ${activeCompany ?? chalk.dim("(none)")}`);
1322
1388
  console.log(` State dir: ${stateDir}`);
1389
+ if (companyFilter)
1390
+ console.log(` Company: ${companyFilter}`);
1323
1391
  console.log(` Journals: ${journals.length}`);
1392
+ if (journals.length === 0 && companyFilter) {
1393
+ console.log(chalk.dim(`No sync journal for company "${companyFilter}" yet — run \`hq sync now --company ${companyFilter}\` to create one.`));
1394
+ return;
1395
+ }
1324
1396
  if (journals.length === 0) {
1325
1397
  console.log(chalk.dim("No sync journal yet — run `hq sync now`, `hq sync push`, or `hq sync pull` to create one."));
1326
1398
  return;
@@ -1355,6 +1427,7 @@ export function registerCloudCommands(program) {
1355
1427
  console.log(` Last sync: ${grandLastSync || "never"}`);
1356
1428
  }
1357
1429
  printReadOnlyLocalOnly(hqRoot, journals);
1430
+ printServerOwnedLocalOnly(hqRoot, journals);
1358
1431
  }
1359
1432
  catch (err) {
1360
1433
  console.error(chalk.red("✗ Status failed:"), err instanceof Error ? err.message : String(err));
@@ -1724,13 +1797,18 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
1724
1797
  else if (row.result) {
1725
1798
  const r = row.result;
1726
1799
  const legRefused = r.planLimit ?? null;
1727
- const status = r.aborted || r.filesExcludedByScope > 0 || (r.filesRefusedStale ?? 0) > 0 || legRefused
1800
+ const status = r.aborted ||
1801
+ r.filesExcludedByScope > 0 ||
1802
+ (r.filesRefusedStale ?? 0) > 0 ||
1803
+ (r.filesUploadFailed ?? 0) > 0 ||
1804
+ legRefused
1728
1805
  ? chalk.yellow("⚠")
1729
1806
  : chalk.green("✓");
1730
1807
  console.log(` ${status} ${row.slug}: ${r.filesUploaded} file(s), ` +
1731
1808
  `${formatBytes(r.bytesUploaded)}, ${r.filesSkipped} skipped, ` +
1732
1809
  `${r.filesDeleted} deleted, ${r.filesExcludedByScope} scope-excluded, ` +
1733
- `${r.filesRefusedStale ?? 0} stale-refused, ${r.conflictPaths.length} conflict(s)` +
1810
+ `${r.filesRefusedStale ?? 0} stale-refused, ${r.filesUploadFailed ?? 0} failed, ` +
1811
+ `${r.conflictPaths.length} conflict(s)` +
1734
1812
  (r.aborted ? " — aborted" : "") +
1735
1813
  (legRefused
1736
1814
  ? ` — ${describeRefusedCount(legRefused)} refused by plan limit`
@@ -1747,11 +1825,16 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
1747
1825
  `(${formatBytes(result.bytesUploaded)}) across ${result.attempted} ` +
1748
1826
  `target(s); ${result.filesDeleted} deleted; ${errored} error(s); ` +
1749
1827
  `${result.filesExcludedByScope} scope-excluded; ` +
1750
- `${result.filesRefusedStale ?? 0} stale-refused` +
1828
+ `${result.filesRefusedStale ?? 0} stale-refused; ` +
1829
+ `${result.filesUploadFailed ?? 0} failed` +
1751
1830
  (refusedTargets > 0
1752
1831
  ? `; new files refused by a plan limit on ${refusedTargets} target(s)`
1753
1832
  : "");
1754
- console.log(errored > 0 || result.filesExcludedByScope > 0 || refusedTargets > 0
1833
+ console.log(errored > 0 ||
1834
+ result.filesExcludedByScope > 0 ||
1835
+ (result.filesRefusedStale ?? 0) > 0 ||
1836
+ (result.filesUploadFailed ?? 0) > 0 ||
1837
+ refusedTargets > 0
1755
1838
  ? chalk.yellow(summary)
1756
1839
  : chalk.green(summary));
1757
1840
  for (const row of result.perCompany) {
@@ -1772,6 +1855,9 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
1772
1855
  for (const line of staleUploadRefusalLines(staleCount, row.result?.filesRefusedStalePaths)) {
1773
1856
  console.log(chalk.yellow(`${row.slug}: ${line}`));
1774
1857
  }
1858
+ for (const line of uploadFailedLines(row.result?.filesUploadFailed ?? 0, row.result?.uploadFailedPaths)) {
1859
+ console.log(chalk.yellow(`${row.slug}: ${line}`));
1860
+ }
1775
1861
  }
1776
1862
  if (errored > 0)
1777
1863
  throw new SyncExitError(1);
@@ -1780,10 +1866,12 @@ async function runPushAll(hqRoot, message, onConflict, skipPersonal, skipCompani
1780
1866
  filesExcludedByScope: result.filesExcludedByScope,
1781
1867
  filesRefusedStale: result.filesRefusedStale ?? 0,
1782
1868
  filesRefusedStalePaths: [...new Set(result.filesRefusedStalePaths ?? [])],
1869
+ filesUploadFailed: result.filesUploadFailed ?? 0,
1783
1870
  };
1784
1871
  return (refusalSummary.planLimitRefused ||
1785
1872
  refusalSummary.filesExcludedByScope > 0 ||
1786
- refusalSummary.filesRefusedStale > 0
1873
+ refusalSummary.filesRefusedStale > 0 ||
1874
+ refusalSummary.filesUploadFailed > 0
1787
1875
  ? refusalSummary
1788
1876
  : null);
1789
1877
  }
@@ -1872,9 +1960,11 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1872
1960
  ...(nowPushPrefixSet !== undefined ? { prefixSet: nowPushPrefixSet } : {}),
1873
1961
  });
1874
1962
  const pushRefused = planLimitTracker.summary();
1963
+ const pushUploadFailed = readUploadFailures(pushResult);
1875
1964
  const pushStatus = pushResult.aborted ||
1876
1965
  pushResult.filesExcludedByScope > 0 ||
1877
1966
  (pushResult.filesRefusedStale ?? 0) > 0 ||
1967
+ pushUploadFailed.count > 0 ||
1878
1968
  pushRefused
1879
1969
  ? chalk.yellow("⚠")
1880
1970
  : chalk.green("✓");
@@ -1895,6 +1985,9 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
1895
1985
  for (const line of staleUploadRefusalLines(pushResult.filesRefusedStale ?? 0, pushResult.filesRefusedStalePaths)) {
1896
1986
  console.log(chalk.yellow(line));
1897
1987
  }
1988
+ for (const line of uploadFailedLines(pushUploadFailed.count, pushUploadFailed.paths)) {
1989
+ console.log(chalk.yellow(line));
1990
+ }
1898
1991
  if (pushResult.aborted) {
1899
1992
  console.log(chalk.yellow("\n⚠ Sync now aborted on push leg; pull skipped."));
1900
1993
  throw new SyncExitError(1);
@@ -2016,7 +2109,8 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
2016
2109
  throw new SyncExitError(1, { health: "succeeded" });
2017
2110
  }
2018
2111
  if (pushResult.filesExcludedByScope > 0 ||
2019
- (pushResult.filesRefusedStale ?? 0) > 0) {
2112
+ (pushResult.filesRefusedStale ?? 0) > 0 ||
2113
+ pushUploadFailed.count > 0) {
2020
2114
  console.log(chalk.yellow("\n⚠ Sync now finished, but one or more requested uploads were refused."));
2021
2115
  throw new SyncExitError(1, { health: "succeeded" });
2022
2116
  }
@@ -2141,6 +2235,52 @@ export function printReadOnlyLocalOnly(hqRoot, journals, log = (line) => console
2141
2235
  }
2142
2236
  return grand;
2143
2237
  }
2238
+ /**
2239
+ * `hq sync status`: list local files under server-owned folders (sources/,
2240
+ * signals/, ontology/ outside entities/) that are not in the sync journal.
2241
+ * The server pipeline writes these folders; a client push refuses them and
2242
+ * the recurring runner skips them quietly, so a file written locally there
2243
+ * never reaches the vault and `pendingFiles: 0` still reports a clean tree
2244
+ * (feedback_0414d71c). Uses only the journals already loaded, with no
2245
+ * network calls. Prints nothing when nothing applies.
2246
+ */
2247
+ export function printServerOwnedLocalOnly(hqRoot, journals, log = (line) => console.log(line)) {
2248
+ let shouldSync;
2249
+ let grand = 0;
2250
+ for (const { slug, journal } of journals) {
2251
+ if (slug === PERSONAL_VAULT_JOURNAL_SLUG)
2252
+ continue;
2253
+ if (!fs.existsSync(path.join(hqRoot, "companies", slug)))
2254
+ continue;
2255
+ if (!shouldSync) {
2256
+ try {
2257
+ shouldSync = createIgnoreFilter(hqRoot);
2258
+ }
2259
+ catch {
2260
+ shouldSync = () => true;
2261
+ }
2262
+ }
2263
+ const journalKeys = new Set(Object.keys(journal.files ?? {}));
2264
+ const found = findServerOwnedLocalOnly({ hqRoot, slug, journalKeys, shouldSync });
2265
+ if (found.total === 0)
2266
+ continue;
2267
+ grand += found.total;
2268
+ log(chalk.yellow(`\n ! ${slug}: ${found.total} local-only file(s) under server-owned folders ` +
2269
+ `(${SERVER_OWNED_FOLDERS.map((f) => `${f}/`).join(", ")}) never upload`));
2270
+ for (const rel of found.files) {
2271
+ log(` ${rel}`);
2272
+ }
2273
+ if (found.total > found.files.length) {
2274
+ log(chalk.dim(` ... and ${found.total - found.files.length} more`));
2275
+ }
2276
+ }
2277
+ if (grand > 0) {
2278
+ log(chalk.dim(" The cloud pipeline writes these folders; push skips them and does not count them as pending. " +
2279
+ "Keep notes you write yourself under knowledge/ (which syncs), or add sources through an " +
2280
+ "integration or ingestion flow and run hq sync pull."));
2281
+ }
2282
+ return grand;
2283
+ }
2144
2284
  function scopeLabel(slug) {
2145
2285
  if (slug === PERSONAL_VAULT_JOURNAL_SLUG)
2146
2286
  return "personal vault";
@@ -13,7 +13,8 @@ function workTrackingFromBody(body) {
13
13
  }
14
14
  /**
15
15
  * Read both switches from GET /company-settings (owner or admin only). The
16
- * server returns effective values, so an absent field means the default (off).
16
+ * server returns effective values, so an absent field means the default
17
+ * (registry off, live on).
17
18
  * A refused or failed read returns null rather than a guess.
18
19
  */
19
20
  async function readWorkTrackingSettings(token, companyUid) {
@@ -28,7 +29,7 @@ async function readWorkTrackingSettings(token, companyUid) {
28
29
  const body = (await res.json().catch(() => ({})));
29
30
  return {
30
31
  workRegistryEnabled: body.workRegistryEnabled === true,
31
- workMeshLiveEnabled: body.workMeshLiveEnabled === true,
32
+ workMeshLiveEnabled: body.workMeshLiveEnabled !== false,
32
33
  };
33
34
  }
34
35
  catch {
@@ -97,7 +98,7 @@ export function registerCompanyCommand(program) {
97
98
  .option("--accounting-enabled <true|false>", "Enable/disable the accounting pack for this company")
98
99
  .option("--checkpoint-gate-enabled <true|false>", "Require (true) or stop requiring (false) a checkpoint before HQ sessions in this company end a turn")
99
100
  .option("--work-registry-enabled <true|false>", "Turn work-session registration on or off for this company (default off). Work tracking needs this and --work-mesh-live-enabled")
100
- .option("--work-mesh-live-enabled <true|false>", "Turn live work tracking on or off for this company (default off). Work tracking needs this and --work-registry-enabled")
101
+ .option("--work-mesh-live-enabled <true|false>", "Turn live work tracking on or off for this company (default on; set false to opt out). Work tracking needs this and --work-registry-enabled")
101
102
  .action(async (opts) => {
102
103
  try {
103
104
  const companySlug = company.opts().company;
@@ -27,6 +27,16 @@ export declare const NO_BACKEND_RETRY_MS: number;
27
27
  * always releases the queue before the lock would be presumed abandoned.
28
28
  */
29
29
  export declare const SIBLING_MAX_RUNTIME_MS: number;
30
+ /**
31
+ * Minimum gap between two maintenance siblings started by the Stop gate's
32
+ * automatic checkpoint (`--trigger stop-gate-auto`) for the same session. That
33
+ * trigger fires at the end of every turn that did not checkpoint, with no files,
34
+ * learnings, or decisions, so a fast-moving session started a full agent run
35
+ * per turn and most runs found nothing new (feedback_90c33997). Inside the
36
+ * window the thread file and stamps are still written; only the sibling is
37
+ * skipped. Explicit checkpoints are never debounced.
38
+ */
39
+ export declare const AUTO_SIBLING_MIN_INTERVAL_MS: number;
30
40
  /**
31
41
  * Queued payloads one sibling run may process, across its launch claim and its
32
42
  * own queue drain. Each one runs the full upgrade flow, so a 50-deep queue in
@@ -80,6 +90,14 @@ export type WriteStampsOptions = {
80
90
  waitForRetry?: (delayMs: number) => void;
81
91
  };
82
92
  export declare function writeStamps(liveRoot: string, sessionId: string | undefined, options?: WriteStampsOptions): string[];
93
+ /**
94
+ * True when an automatic checkpoint for this session already started (or
95
+ * queued) a sibling within AUTO_SIBLING_MIN_INTERVAL_MS. Without a session ID
96
+ * there is no per-session key, so the run is never debounced. A missing or
97
+ * unreadable stamp also counts as "not recent": a wrong answer costs one extra
98
+ * sibling, never a lost checkpoint.
99
+ */
100
+ export declare function autoSiblingRecent(liveRoot: string, trigger: string | undefined, sessionId: string | undefined, nowMs: number): boolean;
83
101
  /**
84
102
  * Backends whose most recent run died without producing a report.
85
103
  *
@@ -90,6 +90,17 @@ const SIBLING_LOCK_TTL_MS = 60 * 60 * 1000;
90
90
  * always releases the queue before the lock would be presumed abandoned.
91
91
  */
92
92
  export const SIBLING_MAX_RUNTIME_MS = 30 * 60 * 1000;
93
+ /**
94
+ * Minimum gap between two maintenance siblings started by the Stop gate's
95
+ * automatic checkpoint (`--trigger stop-gate-auto`) for the same session. That
96
+ * trigger fires at the end of every turn that did not checkpoint, with no files,
97
+ * learnings, or decisions, so a fast-moving session started a full agent run
98
+ * per turn and most runs found nothing new (feedback_90c33997). Inside the
99
+ * window the thread file and stamps are still written; only the sibling is
100
+ * skipped. Explicit checkpoints are never debounced.
101
+ */
102
+ export const AUTO_SIBLING_MIN_INTERVAL_MS = 30 * 60 * 1000;
103
+ const AUTO_TRIGGER = "stop-gate-auto";
93
104
  /** Newest-wins cap so a wedged sibling cannot grow an unbounded queue. */
94
105
  const MAX_PENDING_PAYLOADS = 50;
95
106
  /**
@@ -243,6 +254,8 @@ report instead of guessing.
243
254
  never overwrite) so the next sibling takes it, and record that in the report.
244
255
  NEVER read the queue and truncate it in place: a payload appended between
245
256
  your read and the truncate is lost, and its checkpoint is never enriched.
257
+ A missing or empty file means the queue is empty; skip this step. Do not
258
+ treat it as an error.
246
259
  `;
247
260
  /**
248
261
  * Rendered into the sibling prompt only when the payload names a company. The
@@ -573,6 +586,42 @@ export function writeStamps(liveRoot, sessionId, options = {}) {
573
586
  }
574
587
  return stampPaths;
575
588
  }
589
+ function autoSiblingStampPath(liveRoot, sessionId) {
590
+ const sessionKey = sessionId.replace(/[^A-Za-z0-9._-]/g, "_") || "unknown";
591
+ return path.join(liveRoot, "workspace", "orchestrator", "hook-state", `checkpoint-auto-sibling-${sessionKey}`);
592
+ }
593
+ /**
594
+ * True when an automatic checkpoint for this session already started (or
595
+ * queued) a sibling within AUTO_SIBLING_MIN_INTERVAL_MS. Without a session ID
596
+ * there is no per-session key, so the run is never debounced. A missing or
597
+ * unreadable stamp also counts as "not recent": a wrong answer costs one extra
598
+ * sibling, never a lost checkpoint.
599
+ */
600
+ export function autoSiblingRecent(liveRoot, trigger, sessionId, nowMs) {
601
+ if (trigger !== AUTO_TRIGGER || !sessionId?.trim())
602
+ return false;
603
+ try {
604
+ const last = Number(fs.readFileSync(autoSiblingStampPath(liveRoot, sessionId), "utf8").trim());
605
+ if (!Number.isFinite(last))
606
+ return false;
607
+ const age = nowMs - last;
608
+ return age >= 0 && age < AUTO_SIBLING_MIN_INTERVAL_MS;
609
+ }
610
+ catch {
611
+ return false;
612
+ }
613
+ }
614
+ function stampAutoSibling(liveRoot, trigger, sessionId, nowMs) {
615
+ if (trigger !== AUTO_TRIGGER || !sessionId?.trim())
616
+ return;
617
+ try {
618
+ fs.writeFileSync(autoSiblingStampPath(liveRoot, sessionId), String(nowMs));
619
+ }
620
+ catch {
621
+ // The debounce stamp only saves cost; failing to write it must not fail
622
+ // the checkpoint.
623
+ }
624
+ }
576
625
  /**
577
626
  * Codex Stop-gate rollout scope: every HQ user on the operator domain, rather
578
627
  * than the single account that dogfooded it first. Eligibility is decided on
@@ -1343,6 +1392,15 @@ async function startSibling(liveRoot, input, threadPath, backend) {
1343
1392
  fs.mkdirSync(runDir, { recursive: true });
1344
1393
  const claimPath = path.join(runDir, "pending-claimed.jsonl");
1345
1394
  payload.pending_payloads = limitClaimedPayloads(claimPending(pendingPath, claimPath), claimPath, pendingPath);
1395
+ // The sibling prompt reads and renames this file. Leave an empty queue behind
1396
+ // when the claim moved it away (or none existed), so the sibling never trips
1397
+ // on a missing file. Append mode: a payload queued in the meantime is kept.
1398
+ try {
1399
+ fs.closeSync(fs.openSync(pendingPath, "a"));
1400
+ }
1401
+ catch {
1402
+ // Best effort: the prompt also treats a missing queue as empty.
1403
+ }
1346
1404
  if (!payload.company) {
1347
1405
  let queued;
1348
1406
  try {
@@ -1455,9 +1513,12 @@ async function runCheckpoint(options, command, group, preResolvedHqRoot) {
1455
1513
  // Resolved before resolveBackend so an opted-out run pays for no backend
1456
1514
  // probe: `--backend auto` shells out to every installed CLI for a version.
1457
1515
  const siblingOptOut = options.agent === false || !siblingEnabledBySetting(liveRoot);
1516
+ const autoDebounced = !siblingOptOut && autoSiblingRecent(liveRoot, input.trigger, input.sessionId, Date.now());
1458
1517
  // Validated even when nothing will spawn, so `--backend bogus` is still an error.
1459
1518
  const requestedBackend = assertKnownBackend(options.backend);
1460
- const backend = siblingOptOut ? "none" : await resolveBackend(requestedBackend, liveRoot, input.company, { dryRun: options.dryRun });
1519
+ const backend = siblingOptOut || autoDebounced
1520
+ ? "none"
1521
+ : await resolveBackend(requestedBackend, liveRoot, input.company, { dryRun: options.dryRun });
1461
1522
  const now = new Date();
1462
1523
  const threadId = `T-${formatTimestamp(now)}-auto-${summarySlug(input.summary)}`;
1463
1524
  const threadPath = path.join(liveRoot, "workspace", "threads", `${threadId}.json`);
@@ -1478,7 +1539,7 @@ async function runCheckpoint(options, command, group, preResolvedHqRoot) {
1478
1539
  worker: input.worker,
1479
1540
  },
1480
1541
  stamps: stampPaths.map((stampPath) => path.relative(liveRoot, stampPath)),
1481
- sibling: siblingOptOut ? null : { backend },
1542
+ sibling: siblingOptOut || autoDebounced ? null : { backend },
1482
1543
  });
1483
1544
  return;
1484
1545
  }
@@ -1517,11 +1578,15 @@ async function runCheckpoint(options, command, group, preResolvedHqRoot) {
1517
1578
  if (siblingOptOut) {
1518
1579
  printResult(`checkpoint: sibling disabled (${SIBLING_SETTING})`);
1519
1580
  }
1581
+ else if (autoDebounced) {
1582
+ printResult("checkpoint: sibling skipped (automatic checkpoint; one ran for this session recently)");
1583
+ }
1520
1584
  else if (backend === "none") {
1521
1585
  printResult("checkpoint: sibling disabled (backend none)");
1522
1586
  }
1523
1587
  else {
1524
1588
  await startSibling(liveRoot, input, threadPath, backend);
1589
+ stampAutoSibling(liveRoot, input.trigger, input.sessionId, Date.now());
1525
1590
  }
1526
1591
  }
1527
1592
  }
@@ -1,5 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
- export declare function scanDriverState(stateDir: string, maxFails: number, hqBin: string, run?: typeof spawnSync): string;
2
+ export declare function queueMaxSeconds(env?: NodeJS.ProcessEnv): number;
3
+ export declare function scanDriverState(stateDir: string, maxFails: number, hqBin: string, run?: typeof spawnSync, queueMax?: number): string;
3
4
  export declare function checkDriverStall(stateDir: string, stallWindow: number, hqBin: string, run?: typeof spawnSync): string;
4
5
  export declare function acquirePipelineDriverLock(driverDir: string, pid: number): {
5
6
  acquired: boolean;
@@ -14,6 +15,16 @@ export declare function stalledLaneContext(state: string, worker: string): {
14
15
  };
15
16
  export declare function pendingLaneQuestion(text: string, lane: string): string;
16
17
  export declare function sleep(ms: number, signal?: AbortSignal): Promise<void>;
18
+ /**
19
+ * Move a handoff that failed validation to a numbered sibling
20
+ * (handoffs/<story>-<phase>.invalid.<n>.json) so the worker's evidence is never
21
+ * lost, and write a failed handoff in its place that names the validation
22
+ * error and the kept file. Returns both paths.
23
+ */
24
+ export declare function quarantineInvalidHandoff(state: string, storyId: string, phase: string, handoffPath: string, reason: string): {
25
+ keptPath: string;
26
+ handoffPath: string;
27
+ };
17
28
  export declare function stopDriverLanes(state: string, hq: string, logFile: string, options?: {
18
29
  timeoutMs?: number;
19
30
  pollMs?: number;