@indigoai-us/hq-cli 5.345.156 → 5.345.160

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 (188) hide show
  1. package/CHANGELOG.md +255 -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 +535 -10
  6. package/dist/command-catalog.generated.js +678 -31
  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/access.d.ts +6 -0
  12. package/dist/commands/access.js +18 -1
  13. package/dist/commands/acl-lock.d.ts +41 -0
  14. package/dist/commands/acl-lock.js +28 -0
  15. package/dist/commands/agent-enroll.d.ts +2 -0
  16. package/dist/commands/agent-enroll.js +9 -4
  17. package/dist/commands/agent-kit.js +8 -6
  18. package/dist/commands/agents.js +5 -11
  19. package/dist/commands/billing-sponsor.d.ts +4 -4
  20. package/dist/commands/billing-sponsor.js +11 -11
  21. package/dist/commands/billing-usage.d.ts +22 -8
  22. package/dist/commands/billing-usage.js +57 -22
  23. package/dist/commands/billing.d.ts +7 -7
  24. package/dist/commands/billing.js +13 -13
  25. package/dist/commands/bot.js +7 -4
  26. package/dist/commands/channel-suggest.d.ts +56 -0
  27. package/dist/commands/channel-suggest.js +170 -0
  28. package/dist/commands/channels.js +7 -1
  29. package/dist/commands/cloud-provision.js +2 -2
  30. package/dist/commands/cloud.d.ts +45 -0
  31. package/dist/commands/cloud.js +157 -17
  32. package/dist/commands/company.js +4 -3
  33. package/dist/commands/core-checkpoint-native.d.ts +18 -0
  34. package/dist/commands/core-checkpoint-native.js +67 -2
  35. package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
  36. package/dist/commands/core-pipeline-driver-native.js +131 -5
  37. package/dist/commands/core.js +12 -0
  38. package/dist/commands/crm.js +83 -6
  39. package/dist/commands/db-remote-deps.js +12 -1
  40. package/dist/commands/deploy-access.d.ts +33 -0
  41. package/dist/commands/deploy-access.js +322 -0
  42. package/dist/commands/dm-attach.d.ts +106 -0
  43. package/dist/commands/dm-attach.js +184 -0
  44. package/dist/commands/dm.d.ts +37 -0
  45. package/dist/commands/dm.js +172 -10
  46. package/dist/commands/feedback-show.d.ts +40 -0
  47. package/dist/commands/feedback-show.js +261 -0
  48. package/dist/commands/feedback.d.ts +21 -0
  49. package/dist/commands/feedback.js +152 -2
  50. package/dist/commands/files.d.ts +24 -0
  51. package/dist/commands/files.js +71 -8
  52. package/dist/commands/integrations-connect.js +7 -3
  53. package/dist/commands/lanes-accounts.js +16 -1
  54. package/dist/commands/lanes-loop.js +14 -4
  55. package/dist/commands/lanes-message.js +6 -4
  56. package/dist/commands/lanes-resume.js +36 -13
  57. package/dist/commands/lanes-shared.d.ts +5 -0
  58. package/dist/commands/lanes-shared.js +13 -3
  59. package/dist/commands/lanes-watch.js +1 -0
  60. package/dist/commands/lanes.d.ts +20 -1
  61. package/dist/commands/lanes.js +201 -51
  62. package/dist/commands/meetings.d.ts +4 -4
  63. package/dist/commands/meetings.js +17 -8
  64. package/dist/commands/mesh.js +149 -1
  65. package/dist/commands/monitor.d.ts +1 -0
  66. package/dist/commands/monitor.js +4 -1
  67. package/dist/commands/repo.d.ts +41 -0
  68. package/dist/commands/repo.js +331 -0
  69. package/dist/commands/scan.d.ts +3 -1
  70. package/dist/commands/scan.js +7 -3
  71. package/dist/commands/secrets.d.ts +21 -0
  72. package/dist/commands/secrets.js +114 -14
  73. package/dist/commands/signals.js +17 -5
  74. package/dist/commands/skill.js +6 -6
  75. package/dist/commands/staff/listings.d.ts +41 -0
  76. package/dist/commands/staff/listings.js +160 -0
  77. package/dist/commands/staff/reset-test-account.d.ts +3 -2
  78. package/dist/commands/staff/reset-test-account.js +4 -3
  79. package/dist/commands/whoami.js +1 -1
  80. package/dist/hook-engine-path.js +13 -2
  81. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +3392 -1243
  82. package/dist/hq-cloud-sync-runner/package.json +1 -1
  83. package/dist/lib/agent-kit/run/inbox.js +2 -1
  84. package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
  85. package/dist/lib/agent-kit/wake-secrets.js +32 -0
  86. package/dist/lib/agent-kit/wake.d.ts +4 -0
  87. package/dist/lib/agent-kit/wake.js +49 -3
  88. package/dist/lib/billing/plan-limit-refusals.d.ts +13 -3
  89. package/dist/lib/billing/plan-limit-refusals.js +26 -3
  90. package/dist/lib/billing/plan-limit-upgrade-url.d.ts +8 -0
  91. package/dist/lib/billing/plan-limit-upgrade-url.js +18 -0
  92. package/dist/lib/billing/plan-lock.d.ts +22 -6
  93. package/dist/lib/billing/plan-lock.js +58 -30
  94. package/dist/lib/billing/usage-cost.d.ts +24 -2
  95. package/dist/lib/billing/usage-cost.js +103 -3
  96. package/dist/lib/billing/usage-only.d.ts +37 -1
  97. package/dist/lib/billing/usage-only.js +89 -4
  98. package/dist/lib/billing/usage-pause.d.ts +27 -0
  99. package/dist/lib/billing/usage-pause.js +50 -0
  100. package/dist/lib/bot/inflight.d.ts +7 -1
  101. package/dist/lib/bot/inflight.js +10 -2
  102. package/dist/lib/bot/prompt.js +3 -0
  103. package/dist/lib/bot/run.js +72 -20
  104. package/dist/lib/bot/session.d.ts +14 -0
  105. package/dist/lib/bot/session.js +70 -0
  106. package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
  107. package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
  108. package/dist/lib/capacity/lane-login.d.ts +6 -0
  109. package/dist/lib/capacity/lane-login.js +5 -0
  110. package/dist/lib/core-utils/worktree.js +2 -0
  111. package/dist/lib/daemon/exit-cause.d.ts +43 -0
  112. package/dist/lib/daemon/exit-cause.js +88 -0
  113. package/dist/lib/daemon/handoff.d.ts +2 -0
  114. package/dist/lib/daemon/handoff.js +6 -1
  115. package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
  116. package/dist/lib/daemon/paths.d.ts +2 -0
  117. package/dist/lib/daemon/paths.js +1 -0
  118. package/dist/lib/daemon/run.js +13 -0
  119. package/dist/lib/daemon/supervisor.d.ts +54 -1
  120. package/dist/lib/daemon/supervisor.js +297 -21
  121. package/dist/lib/daemon/unit-holds.d.ts +22 -0
  122. package/dist/lib/daemon/unit-holds.js +86 -0
  123. package/dist/lib/db/control-plane.d.ts +5 -0
  124. package/dist/lib/db/control-plane.js +10 -0
  125. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  126. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  127. package/dist/lib/hook-engine/resolve.d.ts +14 -0
  128. package/dist/lib/hook-engine/resolve.js +55 -1
  129. package/dist/lib/index-render/company-knowledge.js +1 -3
  130. package/dist/lib/index-render/projects.js +1 -3
  131. package/dist/lib/lanes/account-company.d.ts +28 -0
  132. package/dist/lib/lanes/account-company.js +53 -0
  133. package/dist/lib/lanes/accounts.d.ts +22 -3
  134. package/dist/lib/lanes/accounts.js +38 -2
  135. package/dist/lib/lanes/claude-balance.d.ts +6 -1
  136. package/dist/lib/lanes/claude-balance.js +6 -1
  137. package/dist/lib/lanes/claude-credential.d.ts +78 -0
  138. package/dist/lib/lanes/claude-credential.js +163 -0
  139. package/dist/lib/lanes/compaction.d.ts +2 -0
  140. package/dist/lib/lanes/compaction.js +9 -0
  141. package/dist/lib/lanes/config.js +6 -0
  142. package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
  143. package/dist/lib/lanes/hooks-install.js +2 -1
  144. package/dist/lib/lanes/host-capacity.d.ts +2 -0
  145. package/dist/lib/lanes/host-capacity.js +1 -0
  146. package/dist/lib/lanes/keep-alive.d.ts +1 -1
  147. package/dist/lib/lanes/keep-alive.js +3 -1
  148. package/dist/lib/lanes/links.js +10 -4
  149. package/dist/lib/lanes/loop.js +16 -1
  150. package/dist/lib/lanes/node-binary.d.ts +17 -0
  151. package/dist/lib/lanes/node-binary.js +30 -0
  152. package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
  153. package/dist/lib/lanes/outpost-dispatch.js +1 -0
  154. package/dist/lib/lanes/reconcile.js +4 -0
  155. package/dist/lib/lanes/remote-control-lane.js +2 -0
  156. package/dist/lib/lanes/spawn.d.ts +3 -0
  157. package/dist/lib/lanes/spawn.js +119 -9
  158. package/dist/lib/lanes/store.d.ts +6 -0
  159. package/dist/lib/lanes/store.js +107 -18
  160. package/dist/lib/lanes/types.d.ts +2 -0
  161. package/dist/lib/lanes/watch.js +28 -5
  162. package/dist/lib/lanes/watcher-main.js +4 -3
  163. package/dist/lib/lanes/watcher.d.ts +4 -0
  164. package/dist/lib/lanes/watcher.js +110 -21
  165. package/dist/lib/mesh/api.d.ts +32 -0
  166. package/dist/lib/mesh/api.js +75 -0
  167. package/dist/lib/plan-limit-nag.d.ts +0 -16
  168. package/dist/lib/plan-limit-nag.js +17 -18
  169. package/dist/lib/secret-scan/detect.js +9 -0
  170. package/dist/lib/secret-scan/repo-config.d.ts +41 -0
  171. package/dist/lib/secret-scan/repo-config.js +237 -0
  172. package/dist/lib/secret-scan/rules.d.ts +7 -1
  173. package/dist/lib/secret-scan/rules.js +3 -1
  174. package/dist/lib/secret-scan/staged.d.ts +5 -0
  175. package/dist/lib/secret-scan/staged.js +19 -2
  176. package/dist/lib/secret-scan/types.d.ts +2 -0
  177. package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
  178. package/dist/lib/sync/server-owned-local-only.js +75 -0
  179. package/dist/main.d.ts +0 -2
  180. package/dist/main.js +19 -57
  181. package/dist/utils/package-use-lease.js +27 -9
  182. package/dist/utils/plan-gate-error.d.ts +30 -14
  183. package/dist/utils/plan-gate-error.js +60 -46
  184. package/dist/utils/team-upgrade.d.ts +19 -6
  185. package/dist/utils/team-upgrade.js +37 -39
  186. package/dist/utils/version-gate.d.ts +7 -0
  187. package/dist/utils/version-gate.js +66 -0
  188. package/package.json +8 -8
@@ -85,7 +85,7 @@ import { SIGN_IN_RETRY_MS, signInNeededText } from "./runtime-sign-in.js";
85
85
  import { classifyRuntimeError, classifyRuntimeFailure, runtimeDidNotFinishText, runtimeFailureReason, runtimeFailureText, runtimeRepairNotice, withRetrySuggestion, } from "./runtime-failure.js";
86
86
  import { HQ_BOT_AGENT_UID_ENV } from "../../commands/onboard-bot-membership.js";
87
87
  import { captureHeartbeatIdentityMismatch, identityMismatchMessage, isHeartbeatIdentityMismatch, } from "../heartbeat-identity.js";
88
- import { clearBotSession, readBotSession, writeBotSession } from "./session.js";
88
+ import { clearBotSession, ensureBotCompanyScope, readBotSession, writeBotSession } from "./session.js";
89
89
  import { defaultBotStatus, patchBotStatus, readBotStatus } from "./status.js";
90
90
  import { CLI_VERSION } from "../../cli-version.js";
91
91
  import { CLI_UPDATE_RESTART_EXIT_CODE } from "./cli-update.js";
@@ -309,6 +309,8 @@ function defaultSleep(ms, signal) {
309
309
  * one did not: `delivered` is how many parts the person has, so a retry starts
310
310
  * where it stopped instead of repeating what they already read.
311
311
  */
312
+ class CompanyBindingError extends Error {
313
+ }
312
314
  class PartialPostError extends Error {
313
315
  parts;
314
316
  delivered;
@@ -453,6 +455,11 @@ export async function runBot(deps) {
453
455
  // Personal: every turn acts as the owner. Company: every turn acts as the bot.
454
456
  const kind = effectiveBotKind(config);
455
457
  const companies = effectiveBotCompanies(config);
458
+ // A single membership is unambiguous; personal and multi-company bots stay unbound.
459
+ const companySlug = config.companySlug ?? (kind === "company" && companies.length === 1 ? companies[0] : undefined);
460
+ const companyScopeKey = kind === "company" || companySlug !== undefined
461
+ ? JSON.stringify({ kind, companies: [...companies].sort(), companySlug: companySlug ?? null })
462
+ : undefined;
456
463
  const lockDeps = defaultPidLockDeps({
457
464
  pid,
458
465
  ...(deps.isProcessAlive ? { isProcessAlive: deps.isProcessAlive } : {}),
@@ -521,7 +528,7 @@ export async function runBot(deps) {
521
528
  // A restart is the owner's "try again" after signing in: start clean.
522
529
  setStatus({ ...fallback(), pid, state: "starting", canDrainForPromotion: false, runtimeSignIn: null, cliVersion });
523
530
  log("info", `starting ${config.name} on hq-cli ${cliVersion} (${config.runtime}, ${kind} bot${companies.length ? ` of ${companies.join(", ")}` : ""}) as ${config.agentUid} for ${config.ownerUid}`);
524
- log("info", `${runtime.id} binary: ${describeRuntimeBinary(runtime, turnEnv(process.env, kind, config.agentUid, config.companySlug), turnCwd)}`);
531
+ log("info", `${runtime.id} binary: ${describeRuntimeBinary(runtime, turnEnv(process.env, kind, config.agentUid, companySlug), turnCwd)}`);
525
532
  deps.cliUpdate?.startup(log);
526
533
  try {
527
534
  const record = await api.getAgent(config.agentUid);
@@ -541,6 +548,15 @@ export async function runBot(deps) {
541
548
  await finish("failed", 0, `could not verify bot identity: ${err instanceof Error ? err.message : String(err)}`);
542
549
  return { stop: async () => { }, done, status: () => null, idle: () => true };
543
550
  }
551
+ try {
552
+ const scope = ensureBotCompanyScope(dir, companyScopeKey);
553
+ if (scope.reset)
554
+ log("info", "company scope changed or was unverified; archived prior conversations before starting fresh");
555
+ }
556
+ catch {
557
+ await finish("failed", 0, "could not isolate previous company conversations; settle any pending work under the previous settings and check workspace permissions before restarting");
558
+ return { stop: async () => { }, done, status: () => null, idle: () => true };
559
+ }
544
560
  // ── System prompt ──────────────────────────────────────────────────────────
545
561
  // A pack worker's bot gets the pack's "Pack setup" section (and the HQ setup
546
562
  // takeover when the pack asks for it and setup is unfinished).
@@ -563,14 +579,14 @@ export async function runBot(deps) {
563
579
  workerDir: config.workerDir,
564
580
  workerSource: config.workerSource,
565
581
  workerId: config.workerId,
566
- companySlug: config.companySlug,
582
+ companySlug,
567
583
  kind,
568
584
  companies,
569
585
  memoryDir: config.memoryDir,
570
586
  sources: readPromptSources(config.hqRoot, config.workerDir, undefined, {
571
587
  memoryDir: config.memoryDir,
572
588
  workerSource: config.workerSource,
573
- companySlug: config.companySlug,
589
+ companySlug,
574
590
  }),
575
591
  });
576
592
  const systemPromptFile = path.join(dir, "system-prompt.md");
@@ -878,8 +894,18 @@ export async function runBot(deps) {
878
894
  * A DM with no session (or whose resume id is gone) gets the recent thread
879
895
  * prepended so a one-word reply still has the question it answers.
880
896
  */
897
+ const bindingNotices = new Set();
881
898
  const executeTurn = async (prompt, sessionScope, tReceived, stream = {}, historyEventId) => {
882
- const live = readBotConfig(dir) ?? config;
899
+ const fresh = readBotConfig(dir);
900
+ if (!fresh)
901
+ throw new CompanyBindingError(`I couldn't read my company settings. Please restart ${config.name} after restoring its configuration; this work is still pending.`);
902
+ const live = fresh;
903
+ const liveCompanies = effectiveBotCompanies(live);
904
+ const liveCompanySlug = live.companySlug ?? (effectiveBotKind(live) === "company" && liveCompanies.length === 1 ? liveCompanies[0] : undefined);
905
+ if (liveCompanySlug !== companySlug || effectiveBotKind(live) !== kind ||
906
+ JSON.stringify([...liveCompanies].sort()) !== JSON.stringify([...companies].sort())) {
907
+ throw new CompanyBindingError(`My company settings changed. Please restart ${config.name} to load them before I continue; this work is still pending.`);
908
+ }
883
909
  const remembered = readStatus()?.claudePermissionMode;
884
910
  const actsPurelyForOwner = sessionScope === "dm" || sessionScope.startsWith("job:");
885
911
  const composePrompt = async (session) => {
@@ -920,22 +946,22 @@ export async function runBot(deps) {
920
946
  return next;
921
947
  };
922
948
  const runAttempt = async (session, turnPrompt) => {
923
- // Company bind (worker-sourced bots): HQ's company hooks key on
949
+ // Company bind (worker-sourced and single-company bots): HQ's company hooks key on
924
950
  // workspace/sessions/<sid>/meta.yaml, so the session id is fixed up front
925
951
  // and the meta file written before every turn (idempotent; covers session
926
952
  // resets). Claude gets the full hook set via its payload session id;
927
953
  // Codex/Grok only see HQ_SESSION_ID.
928
954
  const sid = session?.sessionId ?? randomUUID();
929
- const env = turnEnv(process.env, kind, config.agentUid, config.companySlug);
955
+ const env = turnEnv(process.env, kind, config.agentUid, companySlug);
930
956
  let bind = { env };
931
- if (config.companySlug) {
957
+ if (companySlug) {
932
958
  try {
933
- ensureBotSessionMeta(config.hqRoot, sid, config.companySlug, undefined, now);
934
- bind = { newSessionId: sid, env: { ...env, HQ_SESSION_ID: sid } };
959
+ ensureBotSessionMeta(config.hqRoot, sid, liveCompanySlug, undefined, now);
935
960
  }
936
- catch (err) {
937
- log("warn", `company bind failed for ${config.companySlug}: ${err instanceof Error ? err.message : String(err)}`);
961
+ catch {
962
+ throw new CompanyBindingError(`I couldn't bind this session to ${companySlug}. Please check the bot's workspace permissions and retry; this work is still pending.`);
938
963
  }
964
+ bind = { newSessionId: sid, env: { ...env, HQ_SESSION_ID: sid } };
939
965
  }
940
966
  const tTurnStart = now().getTime();
941
967
  // Model and thinking level are read fresh each turn, so a change from the
@@ -997,6 +1023,7 @@ export async function runBot(deps) {
997
1023
  v: 1,
998
1024
  runtime: config.runtime,
999
1025
  sessionId: turn.sessionId,
1026
+ ...(companyScopeKey !== undefined ? { companyScopeKey } : {}),
1000
1027
  turns: (session?.turns ?? 0) + 1,
1001
1028
  updatedAt: now().toISOString(),
1002
1029
  }, sessionScope);
@@ -1143,7 +1170,18 @@ export async function runBot(deps) {
1143
1170
  await poster.abandon();
1144
1171
  const excerpt = err instanceof RuntimeError ? err.stderrExcerpt : err instanceof Error ? err.message : String(err);
1145
1172
  const aborted = (err instanceof RuntimeError && err.aborted) || controller.signal.aborted;
1146
- const started = !(err instanceof RuntimeError) || err.started;
1173
+ if (err instanceof CompanyBindingError) {
1174
+ setStatus({ lastError: err.message });
1175
+ log("warn", err.message);
1176
+ if (!bindingNotices.has(args.sessionScope)) {
1177
+ try {
1178
+ await args.sendToPerson(err.message);
1179
+ bindingNotices.add(args.sessionScope);
1180
+ }
1181
+ catch { /* Keep the notice retryable if delivery failed. */ }
1182
+ }
1183
+ }
1184
+ const started = !(err instanceof CompanyBindingError) && (!(err instanceof RuntimeError) || err.started);
1147
1185
  return { outcome: { ok: false, started, aborted, excerpt, error: err }, poster };
1148
1186
  }
1149
1187
  finally {
@@ -1180,7 +1218,7 @@ export async function runBot(deps) {
1180
1218
  }
1181
1219
  await sendToTarget(target, notice.text, rootEventId, "human");
1182
1220
  };
1183
- const postOne = async (target, body, rootEventId, audience) => {
1221
+ const postOne = async (target, body, rootEventId, audience, omitFromHistory = false) => {
1184
1222
  const root = rootEventId ? { rootEventId } : {};
1185
1223
  if (target.kind === "dm") {
1186
1224
  const posted = await api.sendDm({
@@ -1189,7 +1227,7 @@ export async function runBot(deps) {
1189
1227
  ...root,
1190
1228
  ...(audience ? { audience } : {}),
1191
1229
  });
1192
- if (audience !== "agent") {
1230
+ if (audience !== "agent" && !omitFromHistory) {
1193
1231
  const eventId = posted && typeof posted === "object" && "eventId" in posted && typeof posted.eventId === "string"
1194
1232
  ? posted.eventId
1195
1233
  : undefined;
@@ -1235,10 +1273,10 @@ export async function runBot(deps) {
1235
1273
  * PartialPostError naming how many parts got through, so the caller can save
1236
1274
  * exactly the remainder.
1237
1275
  */
1238
- const sendParts = async (target, parts, from, rootEventId, onDelivered, audience) => {
1276
+ const sendParts = async (target, parts, from, rootEventId, onDelivered, audience, omitFromHistory = false) => {
1239
1277
  for (let i = Math.max(0, from); i < parts.length; i += 1) {
1240
1278
  try {
1241
- await postOne(target, parts[i], rootEventId, audience);
1279
+ await postOne(target, parts[i], rootEventId, audience, omitFromHistory);
1242
1280
  }
1243
1281
  catch (err) {
1244
1282
  throw new PartialPostError(parts, i, err);
@@ -1459,7 +1497,7 @@ export async function runBot(deps) {
1459
1497
  const parts = marker.replyParts ?? splitBody(recoveryBody(marker));
1460
1498
  const from = marker.replyParts ? Math.min(Math.max(0, marker.replyPartsDelivered ?? 0), parts.length) : 0;
1461
1499
  try {
1462
- await sendParts(marker.target, parts, from, marker.rootEventId, (count) => patchInflight(dir, marker.messageId, { replyParts: parts, replyPartsDelivered: count }));
1500
+ await sendParts(marker.target, parts, from, marker.rootEventId, (count) => patchInflight(dir, marker.messageId, { replyParts: parts, replyPartsDelivered: count }), undefined, marker.omitFromHistory);
1463
1501
  }
1464
1502
  catch (err) {
1465
1503
  const reason = err instanceof Error ? err.message : String(err);
@@ -1471,12 +1509,12 @@ export async function runBot(deps) {
1471
1509
  // no part of this run reached the chat yet.
1472
1510
  const headerOut = (marker.progressPosted ?? 0) > 0 || (err instanceof PartialPostError ? err.delivered : from) > 0;
1473
1511
  const text = marker.jobHeader && !headerOut ? `${marker.jobHeader}${UNDELIVERABLE_JOB_REPORT_TEXT}` : UNDELIVERABLE_JOB_REPORT_TEXT;
1474
- await postOne(marker.target, text, marker.rootEventId).catch(() => undefined);
1512
+ await postOne(marker.target, text, marker.rootEventId, undefined, marker.omitFromHistory).catch(() => undefined);
1475
1513
  // Giving up on DELIVERY is not giving up on the RUN: report it.
1476
1514
  await reportRecoveredJobRun(marker);
1477
1515
  }
1478
1516
  else {
1479
- await postOne(marker.target, UNDELIVERABLE_REPLY_TEXT, marker.rootEventId).catch(() => undefined);
1517
+ await postOne(marker.target, UNDELIVERABLE_REPLY_TEXT, marker.rootEventId, undefined, marker.omitFromHistory).catch(() => undefined);
1480
1518
  }
1481
1519
  await closeMarker(marker, isKickoff);
1482
1520
  return true;
@@ -1592,6 +1630,10 @@ export async function runBot(deps) {
1592
1630
  workingNotice: false,
1593
1631
  });
1594
1632
  await clearThinking();
1633
+ if (!outcome.ok && outcome.error instanceof CompanyBindingError) {
1634
+ clearInflight(dir, KICKOFF_MESSAGE_ID);
1635
+ return;
1636
+ }
1595
1637
  if (outcome.ok) {
1596
1638
  // The kickoff never runs silenceable, so outcome.silent is never set here.
1597
1639
  if (poster.postedCount === 0) {
@@ -1758,6 +1800,11 @@ export async function runBot(deps) {
1758
1800
  workingNotice: false,
1759
1801
  ...(quietJob ? { suppressOutput: true } : {}),
1760
1802
  });
1803
+ if (!outcome.ok && outcome.error instanceof CompanyBindingError) {
1804
+ clearInflight(dir, id);
1805
+ attempts.delete(id);
1806
+ return;
1807
+ }
1761
1808
  if (outcome.ok) {
1762
1809
  if (!quietJob && !reportDelivered(outcome.reply)) {
1763
1810
  // The work finished but the REPORT did not fully reach the DM — even if
@@ -2076,6 +2123,11 @@ export async function runBot(deps) {
2076
2123
  });
2077
2124
  // The model saw the note once its turn ran. A turn that never started, or
2078
2125
  // one held for a coding-tool sign-in, runs again and keeps the note for it.
2126
+ if (!outcome.ok && outcome.error instanceof CompanyBindingError) {
2127
+ clearInflight(dir, id);
2128
+ attempts.delete(id);
2129
+ return;
2130
+ }
2079
2131
  if (setupNote && !toolOffer && (outcome.ok || (outcome.started && !isSignInFailure(outcome.excerpt)))) {
2080
2132
  try {
2081
2133
  patchSetupIntroState(dir, { notePending: false });
@@ -10,10 +10,24 @@ export interface BotSessionFile {
10
10
  v: 1;
11
11
  runtime: BotRuntimeId;
12
12
  sessionId: string;
13
+ /** Company/membership fingerprint under which this transcript was created. */
14
+ companyScopeKey?: string;
13
15
  turns: number;
14
16
  updatedAt: string;
15
17
  }
16
18
  export declare function readBotSession(dir: string, runtime: BotRuntimeId, scope?: BotSessionScope): BotSessionFile | null;
17
19
  export declare function writeBotSession(dir: string, session: BotSessionFile, scope?: BotSessionScope): void;
18
20
  export declare function clearBotSession(dir: string, scope?: BotSessionScope): void;
21
+ /**
22
+ * Run once at startup, before reading or appending conversation state. A scope
23
+ * change must never resume a prior company's model session or replay its DM
24
+ * history. Preserve those files privately for recovery, then record the new
25
+ * scope. Filesystem failures propagate so callers cannot start a model turn.
26
+ * Legacy personal bots retain their existing history; company bots require
27
+ * positive scope evidence and archive legacy, unproven conversations once.
28
+ */
29
+ export declare function ensureBotCompanyScope(dir: string, companyScopeKey: string | undefined): {
30
+ reset: boolean;
31
+ archiveDir?: string;
32
+ };
19
33
  //# sourceMappingURL=session.d.ts.map
@@ -6,6 +6,8 @@
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
+ import { randomUUID } from "node:crypto";
10
+ import { omitInflightRecoveryHistory, readInflightTurns } from "./inflight.js";
9
11
  import { botSessionPath } from "./paths.js";
10
12
  export function readBotSession(dir, runtime, scope = "dm") {
11
13
  const p = botSessionPath(dir, scope);
@@ -36,4 +38,72 @@ export function clearBotSession(dir, scope = "dm") {
36
38
  /* already gone */
37
39
  }
38
40
  }
41
+ /**
42
+ * Run once at startup, before reading or appending conversation state. A scope
43
+ * change must never resume a prior company's model session or replay its DM
44
+ * history. Preserve those files privately for recovery, then record the new
45
+ * scope. Filesystem failures propagate so callers cannot start a model turn.
46
+ * Legacy personal bots retain their existing history; company bots require
47
+ * positive scope evidence and archive legacy, unproven conversations once.
48
+ */
49
+ export function ensureBotCompanyScope(dir, companyScopeKey) {
50
+ const marker = path.join(dir, "company-scope.json");
51
+ const hasMarker = fs.existsSync(marker);
52
+ if (!hasMarker && companyScopeKey === undefined)
53
+ return { reset: false };
54
+ const expected = companyScopeKey ?? null;
55
+ let provenPreviousScope = false;
56
+ if (hasMarker) {
57
+ // Read errors fail closed. Invalid JSON is unproven provenance and resets.
58
+ const body = fs.readFileSync(marker, "utf8");
59
+ let parsed;
60
+ try {
61
+ parsed = JSON.parse(body);
62
+ }
63
+ catch {
64
+ parsed = null;
65
+ }
66
+ provenPreviousScope = Boolean(parsed && typeof parsed === "object" &&
67
+ parsed.v === 1 &&
68
+ (typeof parsed.companyScopeKey === "string" ||
69
+ parsed.companyScopeKey === null));
70
+ if (parsed && typeof parsed === "object" &&
71
+ parsed.v === 1 &&
72
+ parsed.companyScopeKey === expected) {
73
+ return { reset: false };
74
+ }
75
+ }
76
+ // A known company switch must settle recovery under its previous scope.
77
+ // Legacy bots have no scope evidence: isolate their model context below,
78
+ // but leave delivery/job markers intact for the existing recovery path.
79
+ // Recovery only settles saved work; it never resumes the old model session.
80
+ // Match recovery's tolerant parser so corrupt markers do not brick startup.
81
+ const pending = readInflightTurns(dir, { strictIo: true });
82
+ if (provenPreviousScope && pending.length > 0) {
83
+ throw new Error("Bot company settings changed with pending work. Restore the previous company settings and settle pending work before switching companies.");
84
+ }
85
+ if (!provenPreviousScope)
86
+ omitInflightRecoveryHistory(dir, pending);
87
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
88
+ const names = fs.readdirSync(dir).filter((name) => /^(?:session|dm-history)(?:-[A-Za-z0-9_-]+)?\.json$/.test(name));
89
+ let archiveDir;
90
+ if (names.length) {
91
+ const archiveRoot = path.join(dir, "company-scope-archives");
92
+ fs.mkdirSync(archiveRoot, { recursive: true, mode: 0o700 });
93
+ archiveDir = path.join(archiveRoot, randomUUID());
94
+ fs.mkdirSync(archiveDir, { mode: 0o700 });
95
+ for (const name of names)
96
+ fs.renameSync(path.join(dir, name), path.join(archiveDir, name));
97
+ }
98
+ // Commit the new provenance only after every old conversation was isolated.
99
+ const tmp = path.join(dir, `.company-scope.${randomUUID()}.tmp`);
100
+ try {
101
+ fs.writeFileSync(tmp, `${JSON.stringify({ v: 1, companyScopeKey: expected })}\n`, { mode: 0o600 });
102
+ fs.renameSync(tmp, marker);
103
+ }
104
+ finally {
105
+ fs.rmSync(tmp, { force: true });
106
+ }
107
+ return { reset: names.length > 0, ...(archiveDir ? { archiveDir } : {}) };
108
+ }
39
109
  //# sourceMappingURL=session.js.map
@@ -287,6 +287,12 @@ export declare function tokenFingerprint(token: string): string;
287
287
  export declare function readClaudeRefreshLast(root: string, home: string): ClaudeRefreshLastEntry | null;
288
288
  /** @deprecated prefer {@link revokedMarkerForCurrentCredential}; this is the pre-fingerprint shim. */
289
289
  export declare function isAccountRevokedPersisted(root: string, accountId: string): boolean;
290
+ /**
291
+ * True when the capacity collector has recorded this refresh token as
292
+ * revoked (by token fingerprint, under any account). Lanes use it to tell a
293
+ * dead sign-in from an access token that Claude Code will renew at startup.
294
+ */
295
+ export declare function isRefreshTokenRevoked(root: string, refreshToken: string): boolean;
290
296
  /**
291
297
  * Compare the persisted revoked marker (if any) against the refresh token the
292
298
  * caller is about to use.
@@ -1556,6 +1556,18 @@ export function isAccountRevokedPersisted(root, accountId) {
1556
1556
  const file = readRefreshLast(root);
1557
1557
  return Object.prototype.hasOwnProperty.call(file.revokedAccounts, accountId);
1558
1558
  }
1559
+ /**
1560
+ * True when the capacity collector has recorded this refresh token as
1561
+ * revoked (by token fingerprint, under any account). Lanes use it to tell a
1562
+ * dead sign-in from an access token that Claude Code will renew at startup.
1563
+ */
1564
+ export function isRefreshTokenRevoked(root, refreshToken) {
1565
+ const file = readRefreshLast(root);
1566
+ const fp = tokenFingerprint(refreshToken);
1567
+ if (file.revokedTokens[fp] !== undefined)
1568
+ return true;
1569
+ return Object.values(file.revokedAccounts).some((entry) => entry.fingerprint === fp);
1570
+ }
1559
1571
  /**
1560
1572
  * Compare the persisted revoked marker (if any) against the refresh token the
1561
1573
  * caller is about to use.
@@ -20,6 +20,12 @@ export interface LaneLoginInput {
20
20
  * login confirms sign-in through this probe. Defaults to the lanes probe.
21
21
  */
22
22
  probeProvider?: Ctx["probeProvider"];
23
+ /**
24
+ * Claude access-token expiry check for the post-login status. Defaults to
25
+ * the real Keychain/file read when the default probe is used; a caller that
26
+ * injects a probe passes its own reader or gets no expiry check.
27
+ */
28
+ readClaudeCredential?: Ctx["readClaudeCredential"];
23
29
  }
24
30
  export interface LaneLoginResult {
25
31
  id: string;
@@ -13,6 +13,7 @@ import { createLaneAccountsHandlers } from "../../commands/lanes-accounts.js";
13
13
  import { effectiveAccountHome } from "../lanes/account-home.js";
14
14
  import { canonicalizeHome, DEFAULT_ACCOUNT_HOMES, loadLanesConfig } from "../lanes/config.js";
15
15
  import { resolveLanesHqRoot } from "../lanes/hq-root.js";
16
+ import { readLaneClaudeCredential } from "../lanes/claude-credential.js";
16
17
  import { defaultProbe } from "../lanes/models.js";
17
18
  import { parseLaneProvider } from "../lanes/types.js";
18
19
  const ACCOUNT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
@@ -108,6 +109,10 @@ function loginCtx(input, hqRoot) {
108
109
  existsSync: (target) => fs.existsSync(target),
109
110
  spawnLogin: defaultHomeEnv(input.spawnLogin ?? defaultSpawnLogin, input.homedir ?? os.homedir),
110
111
  probeProvider: defaultHomeProbe(input.probeProvider ?? defaultProbe, input.homedir ?? os.homedir),
112
+ // Always a real field: the Proxy below turns a missing one into a throwing
113
+ // function, which accountStatus would call and report as a failed check.
114
+ readClaudeCredential: input.readClaudeCredential ??
115
+ (input.probeProvider === undefined ? readLaneClaudeCredential : undefined),
111
116
  };
112
117
  return new Proxy(fields, {
113
118
  get(target, prop) {
@@ -42,6 +42,8 @@ export function createWorktree(args = [], options = {}) {
42
42
  }
43
43
  }
44
44
  const die = (message) => { line(stderr, `ERROR: ${message}`); return 1; };
45
+ // Accept "--base origin/<branch>" as well as "--base <branch>"; fetch and ref lookups add origin/ themselves.
46
+ base = base.replace(/^origin\//, "");
45
47
  if (!name)
46
48
  return die("--name is required (kebab-case slug, e.g. 'fix-login-bug')");
47
49
  if (name.includes("/") || name.includes(" "))
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Classifies why a supervised long-running unit stopped, so the supervisor can
3
+ * tell a failure that a restart will not fix (permanent) from one that may
4
+ * clear on its own (transient).
5
+ *
6
+ * Permanent causes stop the restart loop and hold the unit until a manual
7
+ * `hq daemon restart <unit>`, a command change, or a CLI update. Anything not
8
+ * recognised is transient, so an unknown failure keeps the bounded retry.
9
+ *
10
+ * Output is never substring-matched. Only npm's own diagnostic line
11
+ * (`npm error code <CODE>` from npm 10+, `npm ERR! code <CODE>` from older
12
+ * npm) counts, and only for a code in PERMANENT_NPM_CODES. The Desktop app
13
+ * uses the same prefixes and list.
14
+ */
15
+ export type PermanentExitCause = "missing-binary" | "not-executable" | "package-resolution" | "engine-mismatch" | "permission-denied" | "bad-config" | "auth-refused";
16
+ export type ExitCause = {
17
+ kind: "transient";
18
+ } | {
19
+ kind: "permanent";
20
+ cause: PermanentExitCause;
21
+ detail: string;
22
+ };
23
+ export interface ExitEvidence {
24
+ code: number | null;
25
+ signal: string | null;
26
+ /** The launch error, when the process could not be started. */
27
+ spawnError?: unknown;
28
+ /** Output the process wrote during this run only (not earlier runs). */
29
+ outputTail?: string;
30
+ }
31
+ /**
32
+ * npm error codes that a retry cannot fix without a change by a person.
33
+ * Network and cache codes (ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN,
34
+ * ECONNREFUSED, EINTEGRITY, EPERM, EBUSY, ENOSPC) stay transient on purpose.
35
+ */
36
+ export declare const PERMANENT_NPM_CODES: Readonly<Record<string, PermanentExitCause>>;
37
+ /** The first npm error code line naming a permanent code, as `{ code, line }`. */
38
+ export declare function permanentNpmCode(output: string): {
39
+ code: string;
40
+ cause: PermanentExitCause;
41
+ } | undefined;
42
+ export declare function classifyExit(evidence: ExitEvidence): ExitCause;
43
+ //# sourceMappingURL=exit-cause.d.ts.map
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Classifies why a supervised long-running unit stopped, so the supervisor can
3
+ * tell a failure that a restart will not fix (permanent) from one that may
4
+ * clear on its own (transient).
5
+ *
6
+ * Permanent causes stop the restart loop and hold the unit until a manual
7
+ * `hq daemon restart <unit>`, a command change, or a CLI update. Anything not
8
+ * recognised is transient, so an unknown failure keeps the bounded retry.
9
+ *
10
+ * Output is never substring-matched. Only npm's own diagnostic line
11
+ * (`npm error code <CODE>` from npm 10+, `npm ERR! code <CODE>` from older
12
+ * npm) counts, and only for a code in PERMANENT_NPM_CODES. The Desktop app
13
+ * uses the same prefixes and list.
14
+ */
15
+ /**
16
+ * npm error codes that a retry cannot fix without a change by a person.
17
+ * Network and cache codes (ECONNRESET, ETIMEDOUT, ENOTFOUND, EAI_AGAIN,
18
+ * ECONNREFUSED, EINTEGRITY, EPERM, EBUSY, ENOSPC) stay transient on purpose.
19
+ */
20
+ export const PERMANENT_NPM_CODES = {
21
+ ETARGET: "package-resolution", // no version matches the requested range or tag
22
+ E404: "package-resolution", // package or version not in the registry
23
+ EINVALIDPACKAGENAME: "package-resolution",
24
+ EUNSUPPORTEDPROTOCOL: "package-resolution", // unsupported spec, e.g. a bad URL scheme
25
+ EBADENGINE: "engine-mismatch", // engine-strict and node does not satisfy engines
26
+ EACCES: "permission-denied", // cache or prefix not writable by this user
27
+ E401: "auth-refused",
28
+ E403: "auth-refused",
29
+ ENEEDAUTH: "auth-refused",
30
+ };
31
+ const NPM_CODE_PREFIXES = ["npm error code ", "npm ERR! code "];
32
+ /** sysexits.h EX_NOPERM / EX_CONFIG, and the shell's not-executable / not-found codes. */
33
+ const EXIT_CODE_CAUSES = {
34
+ 77: { cause: "auth-refused", detail: "exit code 77 (permission or auth refused)" },
35
+ 78: { cause: "bad-config", detail: "exit code 78 (configuration error)" },
36
+ 126: { cause: "not-executable", detail: "exit code 126 (command not executable)" },
37
+ 127: { cause: "missing-binary", detail: "exit code 127 (command not found)" },
38
+ };
39
+ /** Launch errno codes, read from the error's `code` field only. */
40
+ const SPAWN_ERROR_CAUSES = {
41
+ ENOENT: "missing-binary",
42
+ EACCES: "not-executable",
43
+ ENOEXEC: "not-executable",
44
+ };
45
+ function errorCode(err) {
46
+ if (err && typeof err === "object" && "code" in err && typeof err.code === "string") {
47
+ return err.code;
48
+ }
49
+ return undefined;
50
+ }
51
+ /** The first npm error code line naming a permanent code, as `{ code, line }`. */
52
+ export function permanentNpmCode(output) {
53
+ for (const raw of output.split(/\r?\n/)) {
54
+ const line = raw.trim();
55
+ for (const prefix of NPM_CODE_PREFIXES) {
56
+ if (!line.startsWith(prefix))
57
+ continue;
58
+ const code = line.slice(prefix.length).split(/\s+/)[0] ?? "";
59
+ const cause = Object.prototype.hasOwnProperty.call(PERMANENT_NPM_CODES, code) ? PERMANENT_NPM_CODES[code] : undefined;
60
+ if (cause)
61
+ return { code, cause };
62
+ }
63
+ }
64
+ return undefined;
65
+ }
66
+ export function classifyExit(evidence) {
67
+ if (evidence.spawnError !== undefined) {
68
+ const code = errorCode(evidence.spawnError);
69
+ const cause = code ? SPAWN_ERROR_CAUSES[code] : undefined;
70
+ if (cause)
71
+ return { kind: "permanent", cause, detail: `could not start (${code})` };
72
+ }
73
+ // A process killed by a signal says nothing about its own configuration.
74
+ if (evidence.signal)
75
+ return { kind: "transient" };
76
+ if (evidence.code === 0)
77
+ return { kind: "transient" };
78
+ if (evidence.code !== null) {
79
+ const byCode = EXIT_CODE_CAUSES[evidence.code];
80
+ if (byCode)
81
+ return { kind: "permanent", ...byCode };
82
+ }
83
+ const npm = evidence.outputTail ? permanentNpmCode(evidence.outputTail) : undefined;
84
+ if (npm)
85
+ return { kind: "permanent", cause: npm.cause, detail: `npm error code ${npm.code}` };
86
+ return { kind: "transient" };
87
+ }
88
+ //# sourceMappingURL=exit-cause.js.map
@@ -9,6 +9,8 @@ import { type MachineType } from "./machine-type.js";
9
9
  import { type DaemonPaths } from "./paths.js";
10
10
  /** A live daemon runs `unitId` (or held it after it was stopped from outside). */
11
11
  export declare function daemonSupervises(paths: DaemonPaths, unitId: string, alive?: (pid: number) => boolean): boolean;
12
+ /** Current supervisor hold, including its explicit recovery command; never infer one from stale state. */
13
+ export declare function daemonHoldReason(paths: DaemonPaths, unitId: string, alive?: (pid: number) => boolean): string | null;
12
14
  /**
13
15
  * A live daemon runs the service `unitId` belongs to (`bots` for
14
16
  * `bots:<name>`), so it will run this instance once its plan includes it. For
@@ -16,7 +16,7 @@ import { detectMachineType } from "./machine-type.js";
16
16
  import { daemonPaths } from "./paths.js";
17
17
  import { writeControlRequest } from "./requests.js";
18
18
  /** Statuses where the daemon itself owns the unit's process. */
19
- const OWNED = new Set(["running", "backoff", "exited", "idle", "stopped"]);
19
+ const OWNED = new Set(["running", "backoff", "exited", "idle", "stopped", "held"]);
20
20
  const RECENT_HOSTED_DAEMON_MS = 5 * 60_000;
21
21
  /** The updater's postinstall may run in the short gap before the desktop app relaunches its daemon. */
22
22
  function recentlyHostedDaemon(paths) {
@@ -44,6 +44,11 @@ export function daemonSupervises(paths, unitId, alive = (pid) => isDaemonProcess
44
44
  const unit = liveState(paths, alive)?.units?.find((u) => u.id === unitId);
45
45
  return unit !== undefined && OWNED.has(unit.status);
46
46
  }
47
+ /** Current supervisor hold, including its explicit recovery command; never infer one from stale state. */
48
+ export function daemonHoldReason(paths, unitId, alive = (pid) => isDaemonProcess(pid)) {
49
+ const unit = liveState(paths, alive)?.units?.find((u) => u.id === unitId);
50
+ return unit?.status === "held" ? unit.reason ?? `held; \`hq daemon restart ${unitId}\` retries` : null;
51
+ }
47
52
  /**
48
53
  * A live daemon runs the service `unitId` belongs to (`bots` for
49
54
  * `bots:<name>`), so it will run this instance once its plan includes it. For
@@ -43,14 +43,31 @@ function updateRequestIsActive(requestPath) {
43
43
  const pid = request.pid;
44
44
  if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0)
45
45
  throw new Error("The HQ CLI update request is invalid");
46
+ // Same staleness rule as src/utils/package-use-lease.ts: a request written
47
+ // before the current boot (pid may be reused), or whose pid is gone (ESRCH)
48
+ // or owned by another user (EPERM), is not a live updater.
49
+ let stale = false;
46
50
  try {
47
- process.kill(pid, 0);
51
+ stale = fs.statSync(requestPath).mtimeMs < Date.now() - os.uptime() * 1000 - 5_000;
48
52
  }
49
53
  catch (error) {
50
- if (error.code === "ESRCH") {
51
- fs.rmSync(requestPath, { force: true });
54
+ if (error.code === "ENOENT")
52
55
  return false;
56
+ throw new Error("Unable to read the HQ CLI update request");
57
+ }
58
+ if (!stale) {
59
+ try {
60
+ process.kill(pid, 0);
53
61
  }
62
+ catch (error) {
63
+ const code = error.code;
64
+ if (code === "ESRCH" || code === "EPERM")
65
+ stale = true;
66
+ }
67
+ }
68
+ if (stale) {
69
+ fs.rmSync(requestPath, { force: true });
70
+ return false;
54
71
  }
55
72
  return true;
56
73
  }
@@ -8,6 +8,8 @@ export interface DaemonPaths {
8
8
  configPath: string;
9
9
  remoteControlPoolsPath: string;
10
10
  statePath: string;
11
+ /** Long-running units the supervisor holds down, with why (see unit-holds.ts). */
12
+ holdsPath: string;
11
13
  lockPath: string;
12
14
  logsDir: string;
13
15
  requestsDir: string;
@@ -14,6 +14,7 @@ export function daemonPaths(home = os.homedir(), env = process.env) {
14
14
  configPath: path.join(root, "config.json"),
15
15
  remoteControlPoolsPath: path.join(root, "remote-control-pools.json"),
16
16
  statePath: path.join(root, "state.json"),
17
+ holdsPath: path.join(root, "holds.json"),
17
18
  lockPath: path.join(root, "daemon.pid"),
18
19
  logsDir,
19
20
  requestsDir: path.join(root, "requests"),