@indigoai-us/hq-cli 5.345.147 → 5.345.149

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 (168) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +2 -0
  3. package/assets/bot-workers/setup/worker.yaml +11 -0
  4. package/dist/command-catalog.generated.d.ts +341 -11
  5. package/dist/command-catalog.generated.js +421 -11
  6. package/dist/command-help.js +2 -1
  7. package/dist/commands/access.d.ts +45 -2
  8. package/dist/commands/access.js +537 -223
  9. package/dist/commands/agent-inbox.js +2 -1
  10. package/dist/commands/billing-usage.d.ts +33 -2
  11. package/dist/commands/billing-usage.js +75 -4
  12. package/dist/commands/bot.js +2 -2
  13. package/dist/commands/channels.d.ts +27 -0
  14. package/dist/commands/channels.js +108 -4
  15. package/dist/commands/cloud.js +31 -1
  16. package/dist/commands/core-refresh-vault-access-native.d.ts +33 -0
  17. package/dist/commands/core-refresh-vault-access-native.js +356 -28
  18. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
  19. package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
  20. package/dist/commands/core.js +9 -0
  21. package/dist/commands/daemon.d.ts +7 -0
  22. package/dist/commands/daemon.js +37 -3
  23. package/dist/commands/dm.js +18 -3
  24. package/dist/commands/doctor.d.ts +11 -1
  25. package/dist/commands/doctor.js +56 -1
  26. package/dist/commands/install-global.d.ts +8 -8
  27. package/dist/commands/install-global.js +139 -61
  28. package/dist/commands/lanes-shared.d.ts +3 -1
  29. package/dist/commands/lanes.d.ts +4 -4
  30. package/dist/commands/lanes.js +380 -25
  31. package/dist/commands/library-add.d.ts +31 -1
  32. package/dist/commands/library-add.js +69 -2
  33. package/dist/commands/library.js +37 -3
  34. package/dist/commands/pack-install.d.ts +2 -2
  35. package/dist/commands/pack-install.js +133 -35
  36. package/dist/commands/reindex.js +19 -0
  37. package/dist/commands/sync-manifest.d.ts +2 -9
  38. package/dist/commands/sync-manifest.js +4 -15
  39. package/dist/hook-engine-path.d.ts +4 -0
  40. package/dist/hook-engine-path.js +43 -0
  41. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2783 -2354
  42. package/dist/hq-cloud-sync-runner/package.json +1 -1
  43. package/dist/index.d.ts +10 -0
  44. package/dist/index.js +76 -5
  45. package/dist/lazy-commands.js +2 -0
  46. package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
  47. package/dist/lib/agent-kit/inbox-redact.js +361 -0
  48. package/dist/lib/agent-kit/inbox-state.js +11 -2
  49. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  50. package/dist/lib/agent-kit/run/inbox.js +36 -7
  51. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
  52. package/dist/lib/anywhere-runtime-flag-key.js +9 -0
  53. package/dist/lib/billing/usage-cost.d.ts +31 -1
  54. package/dist/lib/billing/usage-cost.js +46 -0
  55. package/dist/lib/company-skill-wrapper.js +6 -4
  56. package/dist/lib/daemon/install.d.ts +47 -1
  57. package/dist/lib/daemon/install.js +181 -15
  58. package/dist/lib/daemon/lock.d.ts +32 -0
  59. package/dist/lib/daemon/lock.js +55 -2
  60. package/dist/lib/db/local.d.ts +2 -2
  61. package/dist/lib/db/local.js +2 -2
  62. package/dist/lib/db/sql.js +2 -2
  63. package/dist/lib/db/sqlite-binding.d.ts +22 -0
  64. package/dist/lib/db/sqlite-binding.js +168 -0
  65. package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
  66. package/dist/lib/doctor/checks/anywhere.d.ts +15 -0
  67. package/dist/lib/doctor/checks/anywhere.js +77 -11
  68. package/dist/lib/doctor/checks/cli-path.d.ts +12 -0
  69. package/dist/lib/doctor/checks/cli-path.js +46 -0
  70. package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
  71. package/dist/lib/doctor/checks/hook-engine.js +17 -0
  72. package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
  73. package/dist/lib/doctor/checks/sync-health.js +38 -2
  74. package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
  75. package/dist/lib/doctor/checks/temp-homes.js +54 -0
  76. package/dist/lib/doctor/fix/apply.d.ts +90 -1
  77. package/dist/lib/doctor/fix/apply.js +176 -10
  78. package/dist/lib/doctor/fix/json-output.d.ts +4 -1
  79. package/dist/lib/doctor/fix/json-output.js +6 -0
  80. package/dist/lib/doctor/fix/remediation.d.ts +7 -1
  81. package/dist/lib/doctor/fix/remediation.js +23 -1
  82. package/dist/lib/doctor/registry.js +10 -0
  83. package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
  84. package/dist/lib/doctor/temp-home-sweeper.js +418 -0
  85. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  86. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  87. package/dist/lib/flag-registry.js +9 -1
  88. package/dist/lib/flag-snapshot-file.js +8 -3
  89. package/dist/lib/hook-engine/reconcile.d.ts +9 -0
  90. package/dist/lib/hook-engine/reconcile.js +134 -0
  91. package/dist/lib/hook-engine/registration.d.ts +17 -0
  92. package/dist/lib/hook-engine/registration.js +50 -0
  93. package/dist/lib/hook-engine/resolve.d.ts +31 -0
  94. package/dist/lib/hook-engine/resolve.js +134 -0
  95. package/dist/lib/install/claude-targets.d.ts +2 -1
  96. package/dist/lib/install/claude-targets.js +8 -3
  97. package/dist/lib/install/codex-targets.d.ts +3 -0
  98. package/dist/lib/install/codex-targets.js +15 -4
  99. package/dist/lib/install/hook-target.d.ts +9 -0
  100. package/dist/lib/install/hook-target.js +39 -0
  101. package/dist/lib/lanes/admission-host.d.ts +1 -0
  102. package/dist/lib/lanes/admission-host.js +8 -1
  103. package/dist/lib/lanes/admission.d.ts +4 -0
  104. package/dist/lib/lanes/admission.js +6 -2
  105. package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
  106. package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
  107. package/dist/lib/lanes/index.d.ts +2 -2
  108. package/dist/lib/lanes/index.js +1 -1
  109. package/dist/lib/lanes/keep-alive.js +4 -1
  110. package/dist/lib/lanes/local-host-identity.d.ts +40 -0
  111. package/dist/lib/lanes/local-host-identity.js +314 -0
  112. package/dist/lib/lanes/reaper.d.ts +1 -1
  113. package/dist/lib/lanes/reaper.js +7 -0
  114. package/dist/lib/lanes/reconcile.d.ts +4 -0
  115. package/dist/lib/lanes/reconcile.js +16 -8
  116. package/dist/lib/lanes/spawn.d.ts +20 -0
  117. package/dist/lib/lanes/spawn.js +48 -2
  118. package/dist/lib/lanes/status-fields.js +6 -1
  119. package/dist/lib/lanes/store.js +8 -1
  120. package/dist/lib/lanes/throttle.d.ts +49 -6
  121. package/dist/lib/lanes/throttle.js +154 -17
  122. package/dist/lib/lanes/types.d.ts +22 -1
  123. package/dist/lib/lanes/types.js +27 -3
  124. package/dist/lib/lanes/watcher.js +27 -2
  125. package/dist/lib/lanes/worker-continuity.d.ts +2 -0
  126. package/dist/lib/lanes/worker-continuity.js +18 -11
  127. package/dist/lib/manifest-disabled.d.ts +13 -0
  128. package/dist/lib/manifest-disabled.js +19 -0
  129. package/dist/lib/service-manager/launchd.d.ts +1 -0
  130. package/dist/lib/service-manager/launchd.js +1 -1
  131. package/dist/lib/windows/console-encoding.d.ts +24 -0
  132. package/dist/lib/windows/console-encoding.js +83 -0
  133. package/dist/lib/windows/install-followup.d.ts +35 -0
  134. package/dist/lib/windows/install-followup.js +142 -0
  135. package/dist/lib/windows/user-path.d.ts +125 -0
  136. package/dist/lib/windows/user-path.js +275 -0
  137. package/dist/lib/work-context/auto-bind.d.ts +1 -1
  138. package/dist/lib/work-context/auto-bind.js +22 -3
  139. package/dist/lib/work-context/lane-bind.d.ts +31 -0
  140. package/dist/lib/work-context/lane-bind.js +101 -0
  141. package/dist/lib/work-context/organize.js +1 -0
  142. package/dist/lib/work-context/prd-sync.js +7 -0
  143. package/dist/lib/work-context/project.d.ts +36 -10
  144. package/dist/lib/work-context/project.js +130 -41
  145. package/dist/lib/work-context/reconcile.d.ts +6 -0
  146. package/dist/lib/work-context/reconcile.js +25 -5
  147. package/dist/lib/work-context/state.d.ts +6 -0
  148. package/dist/lib/work-context/state.js +29 -1
  149. package/dist/main.js +2 -1
  150. package/dist/unknown-command.d.ts +18 -0
  151. package/dist/unknown-command.js +88 -0
  152. package/dist/utils/cognito-session.d.ts +8 -0
  153. package/dist/utils/cognito-session.js +14 -2
  154. package/dist/utils/copy-tree.d.ts +61 -0
  155. package/dist/utils/copy-tree.js +249 -0
  156. package/dist/utils/environmental-error.js +7 -3
  157. package/dist/utils/hook-trust.d.ts +9 -0
  158. package/dist/utils/hook-trust.js +30 -2
  159. package/dist/utils/npm-invocation.d.ts +27 -0
  160. package/dist/utils/npm-invocation.js +54 -0
  161. package/dist/utils/qmd-native-binding-error.js +3 -1
  162. package/dist/utils/resolve-executable.d.ts +2 -0
  163. package/dist/utils/resolve-executable.js +6 -2
  164. package/dist/utils/safe-link.d.ts +50 -0
  165. package/dist/utils/safe-link.js +300 -0
  166. package/package.json +17 -5
  167. package/scripts/hook-engine-manifest.mjs +22 -0
  168. package/scripts/install-hook-engine.mjs +10 -0
@@ -24,9 +24,9 @@ import { DEFAULT_SERVICES, SERVICE_DESCRIPTIONS, enabledServices, hqCloudUnits }
24
24
  import { applyServiceToggle, effectiveSyncOwner, readDaemonConfig, writeDaemonConfig } from "../lib/daemon/config.js";
25
25
  import { daemonUnitInstalled } from "../lib/daemon/handoff.js";
26
26
  import { adoptLegacyUnits, readAdoptedUnits, restoreLegacyUnits } from "../lib/daemon/migrate.js";
27
- import { ROOT_REFUSAL, SYSTEM_UNIT_DIR, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, } from "../lib/daemon/install.js";
27
+ import { DAEMON_WINDOWS_TASK, ROOT_REFUSAL, SYSTEM_UNIT_DIR, WINDOWS_START_WAIT_MS, daemonRootRefusal, installDaemonUnit, installScopeFor, uninstallDaemonUnit, windowsInteractiveSession, } from "../lib/daemon/install.js";
28
28
  import { describeLegacyUnit, findLegacyUnits } from "../lib/daemon/legacy.js";
29
- import { isDaemonProcess, runningDaemonPid } from "../lib/daemon/lock.js";
29
+ import { isDaemonProcess, liveDaemonLock, runningDaemonPid } from "../lib/daemon/lock.js";
30
30
  import { bundledHqCloud, managedHqCloud, managedHqCloudRoot, readHqCloudCopy, readHqCloudUpdateState } from "../lib/daemon/hq-cloud.js";
31
31
  import { MACHINE_TYPE_MARKER_PATH, detectMachineType, homeFromPasswd, } from "../lib/daemon/machine-type.js";
32
32
  import { daemonPaths, ensurePrivateDir } from "../lib/daemon/paths.js";
@@ -137,6 +137,8 @@ async function loadDefaults(over) {
137
137
  resolveHqRoot: over.resolveHqRoot ?? ((flag) => resolveDaemonHqRoot(flag, { env, cwd: process.cwd(), home })),
138
138
  userHome: over.userHome ?? ((user) => loginHome(user, run)),
139
139
  daemonAlive: over.daemonAlive ?? ((pid) => isDaemonProcess(pid)),
140
+ sleep: over.sleep,
141
+ interactiveSession: over.interactiveSession,
140
142
  windowsUser: over.windowsUser ?? (platform === "win32" ? mesh.resolveWindowsUser(env) : ""),
141
143
  windowsHeadlessLauncher: over.windowsHeadlessLauncher ?? (platform === "win32" ? mesh.resolveWindowsHeadlessLauncher(env) : undefined),
142
144
  systemUnitDir: over.systemUnitDir ?? SYSTEM_UNIT_DIR,
@@ -298,12 +300,42 @@ async function runInstall(d, opts) {
298
300
  d.writeFile(MACHINE_TYPE_MARKER_PATH, `type=${detected.type}\nuser=${runAsUser}\n`);
299
301
  }
300
302
  const runAsHome = runAsUser ? d.userHome(runAsUser) : undefined;
301
- const result = installDaemonUnit(hostFor(d, paths, scope === "user"), { scope, runAsUser, runAsHome }, { run: d.run });
303
+ const result = installDaemonUnit(hostFor(d, paths, scope === "user"), { scope, runAsUser, runAsHome }, {
304
+ run: d.run,
305
+ sleep: d.sleep,
306
+ runningDaemon: () => liveDaemonLock(paths.lockPath, d.daemonAlive),
307
+ });
302
308
  handOver(d, { paths, detected, runAsUser });
309
+ if (result.warning)
310
+ d.err(result.warning);
303
311
  if (result.kind === "manual") {
304
312
  d.out(`This platform has no supported service manager. Start the daemon yourself with:\n ${result.manualCommand}`);
305
313
  return;
306
314
  }
315
+ if (result.startPending) {
316
+ const signedIn = (d.interactiveSession ?? (() => windowsInteractiveSession(d.run, d.windowsUser ?? "")))();
317
+ // A missing daemon is not a failure when the user is signed out, or when
318
+ // Constrained Language Mode refuses the session check; the latter says the
319
+ // check did not run. A check that failed for any other reason (null) is
320
+ // treated like a known sign-in and exits non-zero.
321
+ if (signedIn === false || signedIn === "unavailable") {
322
+ d.out(`Installed: ${result.unitPath}`);
323
+ if (signedIn === "unavailable") {
324
+ d.out("HQ could not check whether you are signed in to Windows, so it could not confirm the daemon started.");
325
+ }
326
+ d.out("The daemon starts at your next sign-in to Windows. It is not running yet.");
327
+ d.out("Check it with: hq daemon status");
328
+ return;
329
+ }
330
+ const seconds = WINDOWS_START_WAIT_MS / 1000;
331
+ throw new DaemonCommandError([
332
+ signedIn
333
+ ? `Installed ${result.unitPath}, but the daemon did not start within ${seconds} seconds although you are signed in to Windows.`
334
+ : `Installed ${result.unitPath}, but the daemon did not start within ${seconds} seconds, and HQ could not check whether you are signed in to Windows.`,
335
+ "Check it with: hq daemon status",
336
+ `The task's last result: powershell -NoProfile -Command "(Get-ScheduledTaskInfo -TaskName '${DAEMON_WINDOWS_TASK}').LastTaskResult"`,
337
+ ].join("\n"));
338
+ }
307
339
  if (!result.running) {
308
340
  throw new DaemonCommandError(`Installed ${result.unitPath}, but it did not start: ${result.error ?? "unknown error"}`);
309
341
  }
@@ -354,6 +386,8 @@ async function runUninstall(d, opts) {
354
386
  }
355
387
  const result = uninstallDaemonUnit(hostFor(d, paths, false), { scope: installScopeFor(detected.type) }, { run: d.run });
356
388
  d.out(result.removed ? `Removed ${result.unitPath}` : "hq daemon was not installed.");
389
+ if (result.warning)
390
+ d.err(result.warning);
357
391
  if (opts.restoreLegacy) {
358
392
  const restored = restoreLegacyUnits(migrate);
359
393
  for (const u of restored.restored)
@@ -4,6 +4,7 @@ import { ensureCognitoToken } from "../utils/cognito-session.js";
4
4
  import { resolveDestinationCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
5
5
  import { isMachineIdentity } from "@indigoai-us/hq-cloud";
6
6
  import { peekIdToken } from "../utils/id-token.js";
7
+ import { redactAgentInboxItem as redactInboxCredentials } from "../lib/agent-kit/inbox-redact.js";
7
8
  import { EMAIL_PATTERN, RECIPIENT_UID_PATTERN } from "../lib/notify-recipient.js";
8
9
  import { dmForwardCliEnabled } from "./dm-forward-flag.js";
9
10
  /**
@@ -1281,9 +1282,15 @@ function formatAgentInboxItem(item) {
1281
1282
  const text = str("text") || str("body") || str("subject");
1282
1283
  return `${chalk.bold(from)} ${chalk.dim(`[${channel}] ${id}${when ? ` ${when}` : ""}`)}\n ${firstLine(text)}`;
1283
1284
  }
1284
- /** Remove delegated execution context before agent inbox items can be emitted. */
1285
+ /**
1286
+ * Stdout for `hq dm` drops the whole actingContext. Credential removal itself
1287
+ * is redactInboxCredentials, shared with the mirror, so a later edit cannot
1288
+ * leave a credential key on one path only. The extra omit stays here because
1289
+ * terminal output must not grow the delegation block back.
1290
+ */
1285
1291
  function redactAgentInboxItem(item) {
1286
- const { actingContext: _actingContext, ...safe } = item;
1292
+ const redacted = redactInboxCredentials(item);
1293
+ const { actingContext: _actingContext, ...safe } = redacted;
1287
1294
  return safe;
1288
1295
  }
1289
1296
  async function runAgentInbox(token, agentUid, opts) {
@@ -1458,7 +1465,15 @@ async function resolveChannel(token, target) {
1458
1465
  async function runDmChannel(target, opts) {
1459
1466
  try {
1460
1467
  const token = await ensureCognitoToken();
1461
- const channel = await resolveChannel(token, target);
1468
+ // A channel id with --json needs no lookup: the id is exact and JSON output
1469
+ // never uses the channel's name. Skipping the list saves fetching every
1470
+ // channel the caller is in (~1000 for an owner), which is what made polled
1471
+ // reads (the super-conductor message wake) take seconds instead of
1472
+ // milliseconds.
1473
+ const rawTarget = target.trim().replace(/^#/, "");
1474
+ const channel = opts.json && /^chn_[A-Za-z0-9]+$/.test(rawTarget)
1475
+ ? { channelId: rawTarget }
1476
+ : await resolveChannel(token, target);
1462
1477
  const channelId = channel.channelId;
1463
1478
  const query = {};
1464
1479
  if (opts.limit)
@@ -25,6 +25,7 @@ import { Command } from "commander";
25
25
  import { type DoctorRegistry } from "../lib/doctor/registry.js";
26
26
  import type { FamilyRun } from "../lib/doctor/types.js";
27
27
  import { type DoctorPlatform } from "../lib/doctor/report.js";
28
+ import { type TempHomeSweepResult } from "../lib/doctor/temp-home-sweeper.js";
28
29
  /**
29
30
  * Marker directories that identify an HQ tree root. A directory is an HQ root
30
31
  * only when it contains ALL of these. `.claude` + `core` are present in both
@@ -126,6 +127,15 @@ export interface RunDoctorResult {
126
127
  export declare function runDoctor(options?: RunDoctorOptions): Promise<RunDoctorResult>;
127
128
  export declare function createClientHealthReasonCodeCompanyResolver(hqRoot: string): (companySlug: string | null) => Promise<boolean>;
128
129
  export declare function createClientHealthCheckStatusCompanyResolver(hqRoot: string): (companySlug: string | null) => Promise<boolean>;
130
+ /** Test seams for {@link registerDoctorCommand}. */
131
+ export interface DoctorCommandDeps {
132
+ /** Runs the temp-home sweep for `--sweep-temp-homes`. */
133
+ sweepTempHomes?: (options: {
134
+ dryRun: boolean;
135
+ }) => TempHomeSweepResult;
136
+ }
137
+ /** Text report for `hq doctor --sweep-temp-homes`. */
138
+ export declare function renderTempHomeSweepText(result: TempHomeSweepResult): string;
129
139
  /** Register the top-level `hq doctor` command so it appears in `hq --help`. */
130
- export declare function registerDoctorCommand(program: Command): void;
140
+ export declare function registerDoctorCommand(program: Command, deps?: DoctorCommandDeps): void;
131
141
  //# sourceMappingURL=doctor.d.ts.map
@@ -37,6 +37,7 @@ import { clientHealthCompanyScopeFromCheckId } from "../lib/doctor/checks/sync-h
37
37
  import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
38
38
  import * as readline from "node:readline";
39
39
  import { runSideEffectTests } from "../lib/doctor/deep/sandbox.js";
40
+ import { sweepTempHomes, } from "../lib/doctor/temp-home-sweeper.js";
40
41
  /**
41
42
  * Marker directories that identify an HQ tree root. A directory is an HQ root
42
43
  * only when it contains ALL of these. `.claude` + `core` are present in both
@@ -315,8 +316,33 @@ export function createClientHealthCheckStatusCompanyResolver(hqRoot) {
315
316
  });
316
317
  };
317
318
  }
319
+ /** Text report for `hq doctor --sweep-temp-homes`. */
320
+ export function renderTempHomeSweepText(result) {
321
+ const lines = [];
322
+ const verb = result.dryRun ? "Would remove" : "Removed";
323
+ for (const entry of result.entries) {
324
+ if (entry.action === "removed" || entry.action === "would-remove") {
325
+ lines.push(` ${verb.toLowerCase()} ${entry.path}`);
326
+ }
327
+ else if (entry.action === "failed") {
328
+ lines.push(` failed ${entry.path}: ${entry.error ?? "unknown error"}`);
329
+ }
330
+ else if (entry.reason === "in-use") {
331
+ lines.push(` kept ${entry.path} (in use)`);
332
+ }
333
+ else if (entry.reason === "in-use-unknown") {
334
+ lines.push(` kept ${entry.path} (could not check whether it is in use: ${entry.error ?? "unknown error"})`);
335
+ }
336
+ }
337
+ const count = result.entries.filter((entry) => entry.action === "removed" || entry.action === "would-remove").length;
338
+ const failed = result.entries.filter((entry) => entry.action === "failed").length;
339
+ lines.push(`${verb} ${count} idle temp home${count === 1 ? "" : "s"}; ` +
340
+ `${result.dryRun ? "would free" : "freed"} ${result.gbFreed.toFixed(2)} GB.` +
341
+ (failed > 0 ? ` ${failed} could not be removed.` : ""));
342
+ return `${lines.join("\n")}\n`;
343
+ }
318
344
  /** Register the top-level `hq doctor` command so it appears in `hq --help`. */
319
- export function registerDoctorCommand(program) {
345
+ export function registerDoctorCommand(program, deps = {}) {
320
346
  program
321
347
  .command("doctor")
322
348
  .description("Verify HQ hook wiring, runtimes, and connection health (read-only; integrations uses one control-plane inventory read).")
@@ -330,9 +356,38 @@ export function registerDoctorCommand(program) {
330
356
  .option("--only <families>", "Run only these comma-separated check families (e.g. hooks). Unknown names are rejected; --json then records the scope.")
331
357
  .option("--fix", "Apply the allowlisted safe repairs (backs up first; read-only without this flag).")
332
358
  .addOption(new Option("--client-health-report", "Resolve the opt-in hq-flags gate used by the client-health report hook.").hideHelp())
359
+ .option("--sweep-temp-homes", "Delete idle temp homes (90+ minutes untouched, not in use): tmp.* folders holding .rustup or .cargo, and hq-temp-home.* folders left by the wrapper. Reports GB freed.")
360
+ .option("--dry-run", "With --sweep-temp-homes, list what would be removed and delete nothing.")
333
361
  .option("--yes", "Skip the interactive --fix confirmation (non-interactive use).")
334
362
  .option("--force", "Let --fix run despite uncommitted changes under .claude/, .codex/, or .grok/.")
335
363
  .action(async (opts, command) => {
364
+ // `--sweep-temp-homes` is a second write path with its own fixed
365
+ // scope (idle, unused temp homes in the temp dirs). Like `--fix`,
366
+ // it refuses a family scope it would otherwise ignore, and it does not
367
+ // combine with `--fix`.
368
+ if (opts.sweepTempHomes === true) {
369
+ if (parseOnly(opts.only) !== undefined || opts.fix === true) {
370
+ process.stderr.write(`hq doctor: --sweep-temp-homes cannot be combined with --only or --fix.\n` +
371
+ ` It removes only idle temp homes in the temp folders; run it on its own.\n`);
372
+ process.exitCode = 1;
373
+ return;
374
+ }
375
+ const sweep = deps.sweepTempHomes ?? ((options) => sweepTempHomes(options));
376
+ const result = sweep({ dryRun: opts.dryRun === true });
377
+ if (opts.json === true) {
378
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
379
+ }
380
+ else {
381
+ process.stdout.write(renderTempHomeSweepText(result));
382
+ }
383
+ process.exitCode = result.entries.some((entry) => entry.action === "failed") ? 1 : 0;
384
+ return;
385
+ }
386
+ if (opts.dryRun === true) {
387
+ process.stderr.write(`hq doctor: --dry-run only applies to --sweep-temp-homes.\n`);
388
+ process.exitCode = 1;
389
+ return;
390
+ }
336
391
  // `--fix` repairs the whole tree from its own allowlist and has no
337
392
  // notion of a check family, so a scope passed alongside it could only
338
393
  // be ignored. Ignoring it silently is the dangerous reading: an
@@ -1,7 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
3
3
  import { type RuntimeHookTrustResult } from '../utils/hook-trust.js';
4
- import { type HookTarget } from '../lib/install/claude-targets.js';
4
+ import { type ShimPathCheck } from '../lib/windows/user-path.js';
5
5
  export interface GlobalInstallOptions {
6
6
  /** HQ checkout to install from (absolute). */
7
7
  hqRoot: string;
@@ -28,14 +28,10 @@ export interface GlobalInstallOptions {
28
28
  marketplaceRepository?: string;
29
29
  /** Test seam for the Claude Code CLI; production uses `claude`. */
30
30
  runClaude?: (args: string[], home: string, env: NodeJS.ProcessEnv) => void;
31
+ /** Platform override for link creation (default process.platform). */
32
+ platform?: NodeJS.Platform;
31
33
  }
32
- /** True when an hq daemon unit (launchd plist or systemd unit) exists. */
33
- export declare function isDaemonInstalled(home: string, platform?: NodeJS.Platform, queryTask?: (task: string) => boolean): boolean;
34
- /**
35
- * US-010: with hqd installed (and the checkout shipping the shim), hooks go to
36
- * the hqd shim; otherwise to master-hook.sh / the Codex adapter.
37
- */
38
- export declare function hookTargetFor(hqRoot: string, home: string, daemonInstalled?: boolean, env?: NodeJS.ProcessEnv): HookTarget;
34
+ export { hookTargetFor, isDaemonInstalled } from '../lib/install/hook-target.js';
39
35
  export interface ClaudePluginSource {
40
36
  sourceDir: string;
41
37
  marketplaceName: string;
@@ -58,6 +54,8 @@ export interface GlobalUninstallOptions {
58
54
  log?: (line: string) => void;
59
55
  /** Test seam for the Claude Code CLI; production uses `claude`. */
60
56
  runClaude?: (args: string[], home: string, env: NodeJS.ProcessEnv) => void;
57
+ /** Platform override for link creation (default process.platform). */
58
+ platform?: NodeJS.Platform;
61
59
  }
62
60
  /** Reverse every install step; non-HQ entries are left untouched. */
63
61
  export declare function uninstallGlobalClaude(opts?: GlobalUninstallOptions): string[];
@@ -83,6 +81,8 @@ export interface GlobalCommandOpts {
83
81
  cwd?: string;
84
82
  /** Test seam for trusting the Codex hooks a global install registers. */
85
83
  trustCodexHooks?: (cwd: string, keys: readonly string[]) => Promise<RuntimeHookTrustResult>;
84
+ /** Test seam: is the npm hq.cmd folder on the Windows PATH? Null off Windows. */
85
+ shimPathCheck?: () => ShimPathCheck | null;
86
86
  }
87
87
  export declare function resolveInstallRoot(explicit: string | undefined, options?: {
88
88
  env?: NodeJS.ProcessEnv;
@@ -1,3 +1,4 @@
1
+ import { hookTargetFor } from '../lib/install/hook-target.js';
1
2
  /**
2
3
  * `hq install --global --runtime claude` / `hq uninstall --global --runtime claude`
3
4
  * (hq-anywhere-runtime US-005).
@@ -17,6 +18,7 @@ import { seedRegistryFromHqRoot } from './link.js';
17
18
  import { resolveHqRoot } from './doctor.js';
18
19
  import { anywhereRuntimeEnabled, AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
19
20
  import { resolveHqMcpEndpoint } from '../lib/daemon/mcp-endpoint.js';
21
+ import { linkDirectory, removeLinkOnly, removeSkillEntry, skillCopySource } from '../utils/safe-link.js';
20
22
  import { chatgptConnectorInstructions } from '../lib/install/chatgpt-targets.js';
21
23
  import { anywhereRuntimeAllowed } from '../lib/anywhere-person-setting.js';
22
24
  import { grokConnectorInstructions } from '../lib/install/grok-targets.js';
@@ -24,33 +26,11 @@ import { readRegistry, registryDir, registryFile, writeRegistry } from '../lib/r
24
26
  import { readCompaniesManifestMap } from '../lib/work-context/repo-remote.js';
25
27
  import { resolveDefaultHqRoot } from '../utils/hq-root-resolver.js';
26
28
  import { trustCodexUserHooks } from '../utils/hook-trust.js';
27
- import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_UNIT_DIR } from '../lib/daemon/install.js';
28
- import { readDaemonConfig } from '../lib/daemon/config.js';
29
- import { daemonPaths } from '../lib/daemon/paths.js';
30
29
  import { assertProviderWrappersInstallable, installProviderWrappers, providerWrapperInstallPaths, removeProviderWrappers } from '../lib/install/provider-wrappers.js';
30
+ import { checkShimOnPath, ensureShimOnUserPath, shimPathWarning } from '../lib/windows/user-path.js';
31
31
  import { addAgentsMdBlock, addCodexHooks, classifyLegacySkills, codexAdapterPath, codexCharterSource, codexHookTrustKeys, codexTargets, hasAllCodexHooks, HQ_OWNED_MARKER, LEGACY_BACKUP_NAME, LEGACY_SKILLS_NAME, removeAgentsMdBlock, removeCodexHooks, } from '../lib/install/codex-targets.js';
32
32
  import { addClaudeMdBlock, addHqHooks, charterPath, claudeTargets, hasAllHqHooks, HQ_ANYWHERE_PACK, HQ_MCP_SERVER_NAME, hqSkillsSource, lineDiff, masterHookPath, shimPath, MCP_SUBPROCESS_PATH, removeClaudeMdBlock, removeHqHooks, SKILL_LINK_PREFIX, } from '../lib/install/claude-targets.js';
33
- /** True when an hq daemon unit (launchd plist or systemd unit) exists. */
34
- export function isDaemonInstalled(home, platform = process.platform, queryTask = (task) => spawnSync('schtasks.exe', ['/Query', '/TN', task], { encoding: 'utf8', windowsHide: true }).status === 0) {
35
- if (platform === 'win32')
36
- return queryTask(DAEMON_WINDOWS_TASK);
37
- const candidates = platform === 'darwin'
38
- ? [path.join(home, 'Library', 'LaunchAgents', `${DAEMON_LAUNCHD_LABEL}.plist`)]
39
- : platform === 'linux'
40
- ? [path.join(home, '.config', 'systemd', 'user', DAEMON_SYSTEMD_UNIT), path.join(SYSTEM_UNIT_DIR, DAEMON_SYSTEMD_UNIT)]
41
- : [];
42
- return candidates.some((p) => fs.existsSync(p));
43
- }
44
- /**
45
- * US-010: with hqd installed (and the checkout shipping the shim), hooks go to
46
- * the hqd shim; otherwise to master-hook.sh / the Codex adapter.
47
- */
48
- export function hookTargetFor(hqRoot, home, daemonInstalled, env = process.env) {
49
- const installed = daemonInstalled ?? isDaemonInstalled(home);
50
- const configuredRoot = readDaemonConfig(daemonPaths(home, env).configPath).hqRoot;
51
- const daemonMatchesRoot = configuredRoot !== undefined && realpathOrSelf(configuredRoot) === realpathOrSelf(hqRoot);
52
- return installed && daemonMatchesRoot && fs.existsSync(shimPath(hqRoot)) ? 'shim' : 'master';
53
- }
33
+ export { hookTargetFor, isDaemonInstalled } from '../lib/install/hook-target.js';
54
34
  const HQ_PACKAGES_MARKETPLACE_REPOSITORY = 'indigoai-us/hq-packages';
55
35
  /** Resolve local overrides first, then the published HQ marketplace. */
56
36
  export function resolveClaudePluginSource(explicitDir, env = process.env, repository = HQ_PACKAGES_MARKETPLACE_REPOSITORY) {
@@ -206,6 +186,22 @@ function mcpManifest(opts) {
206
186
  }
207
187
  return { type: 'stdio', command, args: [entry, 'mcp', 'serve'], env: { PATH: MCP_SUBPROCESS_PATH } };
208
188
  }
189
+ /**
190
+ * The folder an HQ skill entry stands for: a link's resolved target, or the
191
+ * source recorded in a marked copy. Null for anything else.
192
+ */
193
+ function skillEntryTarget(skillsDir, entry) {
194
+ const st = fs.lstatSync(entry);
195
+ if (st.isSymbolicLink())
196
+ return path.resolve(skillsDir, fs.readlinkSync(entry));
197
+ return skillCopySource(entry);
198
+ }
199
+ function reportSkillCopies(kinds, log) {
200
+ const copies = kinds.filter((k) => k === 'copy').length;
201
+ if (copies === 0)
202
+ return;
203
+ log(` ${copies} skill folder(s) were copied because Windows refused both a link and a junction. Copies do not follow HQ skill updates; turn on Developer Mode, then uninstall and install again to get links.`);
204
+ }
209
205
  function skillLinkName(name) {
210
206
  return name.startsWith(SKILL_LINK_PREFIX) ? name : `${SKILL_LINK_PREFIX}${name}`;
211
207
  }
@@ -254,7 +250,7 @@ function planSkills(hqRoot, t) {
254
250
  }
255
251
  if (!st)
256
252
  plan.link.push(name);
257
- else if (st.isSymbolicLink() && path.resolve(t.skillsDir, fs.readlinkSync(link)) === target)
253
+ else if (skillEntryTarget(t.skillsDir, link) === target)
258
254
  plan.already.push(name);
259
255
  else
260
256
  plan.skipped.push(name);
@@ -283,9 +279,8 @@ function hqSkillLinksForRoot(skillsDir, hqRoot) {
283
279
  return names.filter((name) => {
284
280
  const link = path.join(skillsDir, name);
285
281
  try {
286
- if (!fs.lstatSync(link).isSymbolicLink())
287
- return false;
288
- return path.dirname(path.resolve(skillsDir, fs.readlinkSync(link))) === hqSkillsSource(hqRoot);
282
+ const target = skillEntryTarget(skillsDir, link);
283
+ return target !== null && path.dirname(target) === hqSkillsSource(hqRoot);
289
284
  }
290
285
  catch (error) {
291
286
  if (error.code === 'ENOENT')
@@ -301,8 +296,8 @@ function hqCompanySkillLinksForRoot(skillsDir, hqRoot) {
301
296
  const source = hqSkillsSource(hqRoot);
302
297
  return hqSkillLinksForRoot(skillsDir, hqRoot).filter((link) => {
303
298
  try {
304
- const target = path.resolve(skillsDir, fs.readlinkSync(link));
305
- if (path.dirname(target) !== source)
299
+ const target = skillEntryTarget(skillsDir, link);
300
+ if (target === null || path.dirname(target) !== source)
306
301
  return false;
307
302
  const sourceName = path.basename(target);
308
303
  const separator = sourceName.indexOf(':');
@@ -492,22 +487,41 @@ export function installGlobalClaude(opts) {
492
487
  throw error;
493
488
  }
494
489
  }
495
- writeFileAtomic(t.hqPointer, pointerNew);
496
- if (mdOld !== mdNew) {
497
- if (mdNew === null) {
498
- if (fs.existsSync(t.claudeMd))
499
- fs.unlinkSync(t.claudeMd);
490
+ // Skill links go first: they are the step most likely to fail (Windows
491
+ // without symlink privilege), and a failure here must not leave ~/.hq/root
492
+ // or the CLAUDE.md block behind. On any error, undo this run's writes.
493
+ const skillsDirExisted = fs.existsSync(t.skillsDir);
494
+ const createdLinks = [];
495
+ const linkKinds = [];
496
+ try {
497
+ if (!pluginSource)
498
+ fs.mkdirSync(t.skillsDir, { recursive: true });
499
+ for (const name of skills.link) {
500
+ const link = path.join(t.skillsDir, skillLinkName(name));
501
+ linkKinds.push(linkDirectory(path.join(hqSkillsSource(hqRoot), name), link, { platform: opts.platform }));
502
+ createdLinks.push(link);
503
+ }
504
+ writeFileAtomic(t.hqPointer, pointerNew);
505
+ if (mdOld !== mdNew) {
506
+ if (mdNew === null) {
507
+ if (fs.existsSync(t.claudeMd))
508
+ fs.unlinkSync(t.claudeMd);
509
+ }
510
+ else
511
+ writeFileAtomic(t.claudeMd, mdNew);
500
512
  }
501
- else
502
- writeFileAtomic(t.claudeMd, mdNew);
503
513
  }
504
- if (!pluginSource)
505
- fs.mkdirSync(t.skillsDir, { recursive: true });
506
- for (const name of skills.link) {
507
- fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, skillLinkName(name)));
514
+ catch (error) {
515
+ rollbackLinkPhase({
516
+ createdLinks,
517
+ skillsDir: skillsDirExisted ? null : t.skillsDir,
518
+ files: [[t.hqPointer, pointerOld], [t.claudeMd, mdOld]],
519
+ log,
520
+ });
521
+ throw error;
508
522
  }
509
523
  for (const link of skillLinksToRemove)
510
- fs.unlinkSync(link);
524
+ removeSkillEntry(link, log);
511
525
  if (pluginSource) {
512
526
  // Migrate a prior direct install: remove only our old hook/MCP entries while
513
527
  // retaining the original user settings for a later uninstall restore.
@@ -574,16 +588,49 @@ export function installGlobalClaude(opts) {
574
588
  installProviderWrappers(providerBin, home);
575
589
  log(`HQ installed for Claude Code from ${hqRoot}.`);
576
590
  log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
591
+ reportSkillCopies(linkKinds, log);
577
592
  reportSkippedSkills(skills, log, false);
578
593
  if (pluginSource)
579
594
  log(` plugin: ${pluginSource.pluginId} installed from ${pluginSource.sourceDir}; direct hooks and MCP entries removed`);
580
595
  else {
581
- log(` hooks: ${hookTarget === 'shim' ? 'hqd-hook-shim.sh' : 'master-hook.sh'} registered in ${t.settingsJson}`);
596
+ log(` hooks: ${typeof hookTarget === 'object' ? hookTarget.engine : hookTarget === 'shim' ? 'hqd-hook-shim.sh' : 'master-hook.sh'} registered in ${t.settingsJson}`);
582
597
  log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.claudeJson}`);
583
598
  }
584
599
  log(` registry: ${seeded.length} manifest entr${seeded.length === 1 ? 'y' : 'ies'} seeded`);
585
600
  return touched;
586
601
  }
602
+ /**
603
+ * Undo the link phase of a failed install: remove only the links this run
604
+ * created (never recursing into them), put each file back to its pre-run
605
+ * bytes, and drop the skills folder if this run created it and it is empty.
606
+ * A copy that is kept instead of removed is reported through `log`.
607
+ */
608
+ function rollbackLinkPhase(args) {
609
+ for (const link of [...args.createdLinks].reverse()) {
610
+ try {
611
+ removeSkillEntry(link, args.log);
612
+ }
613
+ catch { /* keep rolling back */ }
614
+ }
615
+ for (const [file, original] of args.files) {
616
+ try {
617
+ if (original === null) {
618
+ if (fs.existsSync(file))
619
+ fs.unlinkSync(file);
620
+ }
621
+ else if (readOrNull(file) !== original)
622
+ writeFileAtomic(file, original);
623
+ }
624
+ catch { /* keep rolling back */ }
625
+ }
626
+ if (args.skillsDir) {
627
+ try {
628
+ if (fs.readdirSync(args.skillsDir).length === 0)
629
+ fs.rmdirSync(args.skillsDir);
630
+ }
631
+ catch { /* keep rolling back */ }
632
+ }
633
+ }
587
634
  /** Reverse every install step; non-HQ entries are left untouched. */
588
635
  export function uninstallGlobalClaude(opts = {}) {
589
636
  const home = resolveEnv({ home: opts.home }).home;
@@ -621,11 +668,11 @@ export function uninstallGlobalClaude(opts = {}) {
621
668
  const links = [];
622
669
  try {
623
670
  for (const d of fs.readdirSync(t.skillsDir, { withFileTypes: true })) {
624
- if (!d.name.startsWith(SKILL_LINK_PREFIX) || !d.isSymbolicLink())
671
+ if (!d.name.startsWith(SKILL_LINK_PREFIX) || !(d.isSymbolicLink() || d.isDirectory()))
625
672
  continue;
626
673
  const link = path.join(t.skillsDir, d.name);
627
- const target = path.resolve(t.skillsDir, fs.readlinkSync(link));
628
- if (path.dirname(target) === source)
674
+ const target = skillEntryTarget(t.skillsDir, link);
675
+ if (target !== null && path.dirname(target) === source)
629
676
  links.push(link);
630
677
  }
631
678
  }
@@ -693,7 +740,7 @@ export function uninstallGlobalClaude(opts = {}) {
693
740
  }
694
741
  }
695
742
  for (const link of links)
696
- fs.unlinkSync(link);
743
+ removeSkillEntry(link, log);
697
744
  if (md !== null && mdNext !== md) {
698
745
  if (mdNext === null || (mdNext === '' && state?.claudeMd.original == null))
699
746
  fs.unlinkSync(t.claudeMd);
@@ -842,12 +889,34 @@ export function installGlobalCodex(opts) {
842
889
  log(lineDiff(codexConfigFormat.serialize(tomlDoc), codexConfigFormat.serialize(tomlNext)));
843
890
  return touched;
844
891
  }
845
- writeFileAtomic(hqPointer, pointerNew);
846
- writeFileAtomic(t.agentsMd, mdNew);
892
+ // Skill links go first so a link failure leaves no pointer or AGENTS.md
893
+ // block behind (see installGlobalClaude).
894
+ const skillsDirExisted = fs.existsSync(t.skillsDir);
895
+ const createdLinks = [];
896
+ const linkKinds = [];
897
+ try {
898
+ fs.mkdirSync(t.skillsDir, { recursive: true });
899
+ for (const name of skills.link) {
900
+ const link = path.join(t.skillsDir, skillLinkName(name));
901
+ linkKinds.push(linkDirectory(path.join(hqSkillsSource(hqRoot), name), link, { platform: opts.platform }));
902
+ createdLinks.push(link);
903
+ }
904
+ writeFileAtomic(hqPointer, pointerNew);
905
+ writeFileAtomic(t.agentsMd, mdNew);
906
+ }
907
+ catch (error) {
908
+ rollbackLinkPhase({
909
+ createdLinks,
910
+ skillsDir: skillsDirExisted ? null : t.skillsDir,
911
+ files: [[hqPointer, pointerOld], [t.agentsMd, mdOld]],
912
+ log,
913
+ });
914
+ throw error;
915
+ }
847
916
  // Legacy mirror: replace only when HQ-owned; record enough to put it back.
848
917
  let legacyRecord = prior?.legacy ?? { kind: 'none' };
849
918
  if (legacy.kind === 'hq-symlink') {
850
- fs.unlinkSync(legacyPath);
919
+ removeLinkOnly(legacyPath);
851
920
  legacyRecord = { kind: 'symlink', target: legacy.target };
852
921
  }
853
922
  else if (legacy.kind === 'hq-dir') {
@@ -857,12 +926,8 @@ export function installGlobalCodex(opts) {
857
926
  }
858
927
  else if (legacy.kind === 'foreign')
859
928
  log(legacyWarning(t.skillsDir));
860
- fs.mkdirSync(t.skillsDir, { recursive: true });
861
- for (const name of skills.link) {
862
- fs.symlinkSync(path.join(hqSkillsSource(hqRoot), name), path.join(t.skillsDir, skillLinkName(name)));
863
- }
864
929
  for (const link of companySkillLinksToRemove)
865
- fs.unlinkSync(link);
930
+ removeSkillEntry(link, log);
866
931
  writeConfigAtomic({
867
932
  target: t.hooksJson,
868
933
  pack: HQ_ANYWHERE_PACK,
@@ -905,8 +970,9 @@ export function installGlobalCodex(opts) {
905
970
  installProviderWrappers(providerBin, home);
906
971
  log(`HQ installed for Codex from ${hqRoot}.`);
907
972
  log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
973
+ reportSkillCopies(linkKinds, log);
908
974
  reportSkippedSkills(skills, log, false);
909
- log(` hooks: ${hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
975
+ log(` hooks: ${typeof hookTarget === 'object' ? hookTarget.engine : hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
910
976
  log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.configToml}`);
911
977
  return touched;
912
978
  }
@@ -943,11 +1009,11 @@ export function uninstallGlobalCodex(opts = {}) {
943
1009
  const links = [];
944
1010
  try {
945
1011
  for (const d of fs.readdirSync(t.skillsDir, { withFileTypes: true })) {
946
- if (!d.name.startsWith(SKILL_LINK_PREFIX) || !d.isSymbolicLink())
1012
+ if (!d.name.startsWith(SKILL_LINK_PREFIX) || !(d.isSymbolicLink() || d.isDirectory()))
947
1013
  continue;
948
1014
  const link = path.join(t.skillsDir, d.name);
949
- const target = path.resolve(t.skillsDir, fs.readlinkSync(link));
950
- if (path.dirname(target) === source)
1015
+ const target = skillEntryTarget(t.skillsDir, link);
1016
+ if (target !== null && path.dirname(target) === source)
951
1017
  links.push(link);
952
1018
  }
953
1019
  }
@@ -1012,10 +1078,10 @@ export function uninstallGlobalCodex(opts = {}) {
1012
1078
  }
1013
1079
  }
1014
1080
  for (const link of links)
1015
- fs.unlinkSync(link);
1081
+ removeSkillEntry(link, log);
1016
1082
  if (restoreLegacy && state) {
1017
1083
  if (state.legacy.kind === 'symlink')
1018
- fs.symlinkSync(state.legacy.target, legacyPath);
1084
+ linkDirectory(state.legacy.target, legacyPath, { platform: opts.platform, allowCopy: false });
1019
1085
  else if (state.legacy.kind === 'dir' && fs.existsSync(state.legacy.backup))
1020
1086
  fs.renameSync(state.legacy.backup, legacyPath);
1021
1087
  }
@@ -1124,6 +1190,18 @@ export async function runInstallGlobal(opts) {
1124
1190
  });
1125
1191
  if (runtime === 'codex' && !opts.dryRun)
1126
1192
  await trustInstalledCodexHooks(hqRoot, opts);
1193
+ if (!opts.dryRun) {
1194
+ try {
1195
+ ensureShimOnUserPath({ env, entry: process.argv[1] });
1196
+ }
1197
+ catch {
1198
+ // The warning below is the fallback when the user Path cannot be updated.
1199
+ }
1200
+ const shimPath = (opts.shimPathCheck ?? (() => checkShimOnPath({ env })))();
1201
+ if (shimPath)
1202
+ for (const line of shimPathWarning(shimPath))
1203
+ (opts.log ?? console.log)(line);
1204
+ }
1127
1205
  }
1128
1206
  /**
1129
1207
  * Codex skips untrusted hooks, so the hooks.json entries written above do
@@ -1,7 +1,7 @@
1
1
  import type { LaneCompanyRef, LanePlacementPolicy, LaneRecord, LanesConfig, QuestionAnswer, QuestionBody, QuestionRecord, SeniorRef, AccountHistoryEntry, LaneAccountSelection, PendingResume, QueuedReason, ResolvedLaneWorker, LivenessDeps, MachineIdentity, MonitorEvidenceReader, ReconcileHost, RunCommand, ThrottleDeps, withLaneAdmissionLock, withLaneHireLocks } from "../lib/lanes/index.js";
2
2
  import type { OutpostIdentity } from "../lib/lanes/admission-host.js";
3
3
  import type { OutpostDispatchResult } from "../lib/lanes/outpost-dispatch.js";
4
- import type { CpuBusySource } from "../lib/lanes/throttle.js";
4
+ import type { CpuBusySource, MemoryHold } from "../lib/lanes/throttle.js";
5
5
  import type { ProbeCommand } from "../lib/lanes/models.js";
6
6
  import type { FocusGuardReason } from "../lib/lanes/focus-guard.js";
7
7
  import type { KillProcessFn, ProveDetachedFn, SpawnProcessFn } from "../lib/lanes/spawn.js";
@@ -100,6 +100,8 @@ export interface AdmissionResult {
100
100
  last_busy: number | null;
101
101
  busy_source: CpuBusySource | undefined;
102
102
  sampled_at: string | undefined;
103
+ /** Present when the memory gate holds: the signal, its value and the threshold. */
104
+ memory_hold?: MemoryHold;
103
105
  };
104
106
  }
105
107
  export interface PrepareAccountMoveInput {
@@ -136,12 +136,12 @@ export declare function casUpdateLane(hqRoot: string, laneId: string, updater: (
136
136
  * reconcile pass resumes the lane once every question is answered.
137
137
  */
138
138
  export declare function checkpointInstruction(worker: Pick<ResolvedLaneWorker, "approvalRequired" | "humanCheckpoints">): string;
139
- export declare function composeWorkerBrief(worker: ResolvedLaneWorker, taskBrief: string): string;
139
+ export declare function composeWorkerBrief(worker: ResolvedLaneWorker, taskBrief: string, hqRoot: string): string;
140
140
  /**
141
- * A lane with no worker still carries the standing authorization paragraph
142
- * and the foreground-only rule.
141
+ * A lane with no worker still carries the standing authorization paragraph,
142
+ * the foreground-only rule and the temp-home rule.
143
143
  */
144
- export declare function composeGeneralBrief(taskBrief: string): string;
144
+ export declare function composeGeneralBrief(taskBrief: string, hqRoot: string): string;
145
145
  export declare const DISPATCH_TERMINAL_REASON: "outpost_dispatch_failed";
146
146
  /**
147
147
  * The CAS updater a pickup failure applies. A lane that is already terminal