@indigoai-us/hq-cli 5.345.146 → 5.345.148

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 (138) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +14 -0
  3. package/assets/bot-workers/setup/worker.yaml +11 -0
  4. package/dist/command-catalog.generated.d.ts +326 -4
  5. package/dist/command-catalog.generated.js +403 -4
  6. package/dist/commands/agent-inbox.js +2 -1
  7. package/dist/commands/auth.js +4 -2
  8. package/dist/commands/bot.js +1 -1
  9. package/dist/commands/cloud.js +31 -1
  10. package/dist/commands/core-refresh-vault-access-native.d.ts +17 -0
  11. package/dist/commands/core-refresh-vault-access-native.js +214 -19
  12. package/dist/commands/core-work-mesh-bind-trusted-native.d.ts +9 -0
  13. package/dist/commands/core-work-mesh-bind-trusted-native.js +26 -19
  14. package/dist/commands/core.js +6 -0
  15. package/dist/commands/daemon.d.ts +7 -0
  16. package/dist/commands/daemon.js +37 -3
  17. package/dist/commands/dm.js +9 -2
  18. package/dist/commands/doctor.d.ts +11 -1
  19. package/dist/commands/doctor.js +56 -1
  20. package/dist/commands/files.js +10 -6
  21. package/dist/commands/install-global.d.ts +7 -0
  22. package/dist/commands/install-global.js +129 -35
  23. package/dist/commands/lanes-shared.d.ts +3 -1
  24. package/dist/commands/lanes.d.ts +4 -4
  25. package/dist/commands/lanes.js +380 -25
  26. package/dist/commands/login.js +4 -3
  27. package/dist/commands/pack-install.d.ts +2 -2
  28. package/dist/commands/pack-install.js +133 -35
  29. package/dist/commands/sync-manifest.d.ts +2 -9
  30. package/dist/commands/sync-manifest.js +4 -15
  31. package/dist/hook-engine-path.d.ts +4 -0
  32. package/dist/hook-engine-path.js +36 -0
  33. package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2784 -2360
  34. package/dist/hq-cloud-sync-runner/package.json +1 -1
  35. package/dist/index.js +6 -0
  36. package/dist/lib/agent-kit/inbox-redact.d.ts +44 -0
  37. package/dist/lib/agent-kit/inbox-redact.js +361 -0
  38. package/dist/lib/agent-kit/inbox-state.js +11 -2
  39. package/dist/lib/agent-kit/mcp/tools.js +2 -1
  40. package/dist/lib/agent-kit/run/inbox.js +36 -7
  41. package/dist/lib/anywhere-runtime-flag-key.d.ts +3 -0
  42. package/dist/lib/anywhere-runtime-flag-key.js +9 -0
  43. package/dist/lib/bot/daemon.js +3 -0
  44. package/dist/lib/bot/runtime/index.js +3 -1
  45. package/dist/lib/company-skill-wrapper.js +6 -4
  46. package/dist/lib/daemon/install.d.ts +47 -1
  47. package/dist/lib/daemon/install.js +181 -15
  48. package/dist/lib/daemon/lock.d.ts +32 -0
  49. package/dist/lib/daemon/lock.js +55 -2
  50. package/dist/lib/doctor/__testing__/run-doctor.js +4 -0
  51. package/dist/lib/doctor/checks/anywhere.d.ts +3 -0
  52. package/dist/lib/doctor/checks/anywhere.js +23 -8
  53. package/dist/lib/doctor/checks/cli-path.d.ts +11 -0
  54. package/dist/lib/doctor/checks/cli-path.js +45 -0
  55. package/dist/lib/doctor/checks/hook-engine.d.ts +4 -0
  56. package/dist/lib/doctor/checks/hook-engine.js +16 -0
  57. package/dist/lib/doctor/checks/release-links.d.ts +23 -0
  58. package/dist/lib/doctor/checks/release-links.js +152 -0
  59. package/dist/lib/doctor/checks/sync-health.d.ts +7 -0
  60. package/dist/lib/doctor/checks/sync-health.js +38 -2
  61. package/dist/lib/doctor/checks/temp-homes.d.ts +19 -0
  62. package/dist/lib/doctor/checks/temp-homes.js +54 -0
  63. package/dist/lib/doctor/fix/apply.d.ts +90 -1
  64. package/dist/lib/doctor/fix/apply.js +176 -10
  65. package/dist/lib/doctor/fix/json-output.d.ts +4 -1
  66. package/dist/lib/doctor/fix/json-output.js +6 -0
  67. package/dist/lib/doctor/fix/remediation.d.ts +7 -1
  68. package/dist/lib/doctor/fix/remediation.js +23 -1
  69. package/dist/lib/doctor/registry.js +13 -0
  70. package/dist/lib/doctor/temp-home-sweeper.d.ts +149 -0
  71. package/dist/lib/doctor/temp-home-sweeper.js +418 -0
  72. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  73. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  74. package/dist/lib/flag-registry.js +9 -1
  75. package/dist/lib/flag-snapshot-file.js +8 -3
  76. package/dist/lib/hook-engine/resolve.d.ts +31 -0
  77. package/dist/lib/hook-engine/resolve.js +134 -0
  78. package/dist/lib/lanes/admission-host.d.ts +1 -0
  79. package/dist/lib/lanes/admission-host.js +8 -1
  80. package/dist/lib/lanes/admission.d.ts +4 -0
  81. package/dist/lib/lanes/admission.js +6 -2
  82. package/dist/lib/lanes/envelope-repair-dispatch.d.ts +5 -0
  83. package/dist/lib/lanes/envelope-repair-dispatch.js +12 -1
  84. package/dist/lib/lanes/index.d.ts +2 -2
  85. package/dist/lib/lanes/index.js +1 -1
  86. package/dist/lib/lanes/keep-alive.js +4 -1
  87. package/dist/lib/lanes/local-host-identity.d.ts +40 -0
  88. package/dist/lib/lanes/local-host-identity.js +314 -0
  89. package/dist/lib/lanes/reaper.d.ts +1 -1
  90. package/dist/lib/lanes/reaper.js +7 -0
  91. package/dist/lib/lanes/reconcile.d.ts +4 -0
  92. package/dist/lib/lanes/reconcile.js +16 -8
  93. package/dist/lib/lanes/spawn.d.ts +20 -0
  94. package/dist/lib/lanes/spawn.js +48 -2
  95. package/dist/lib/lanes/status-fields.js +6 -1
  96. package/dist/lib/lanes/store.js +8 -1
  97. package/dist/lib/lanes/throttle.d.ts +49 -6
  98. package/dist/lib/lanes/throttle.js +154 -17
  99. package/dist/lib/lanes/types.d.ts +22 -1
  100. package/dist/lib/lanes/types.js +27 -3
  101. package/dist/lib/lanes/watcher.js +27 -2
  102. package/dist/lib/lanes/worker-continuity.d.ts +2 -0
  103. package/dist/lib/lanes/worker-continuity.js +18 -11
  104. package/dist/lib/manifest-disabled.d.ts +13 -0
  105. package/dist/lib/manifest-disabled.js +19 -0
  106. package/dist/lib/service-manager/launchd.d.ts +1 -0
  107. package/dist/lib/service-manager/launchd.js +1 -1
  108. package/dist/lib/windows/user-path.d.ts +48 -0
  109. package/dist/lib/windows/user-path.js +114 -0
  110. package/dist/lib/work-context/auto-bind.d.ts +1 -1
  111. package/dist/lib/work-context/auto-bind.js +22 -3
  112. package/dist/lib/work-context/lane-bind.d.ts +31 -0
  113. package/dist/lib/work-context/lane-bind.js +101 -0
  114. package/dist/lib/work-context/organize.js +1 -0
  115. package/dist/lib/work-context/prd-sync.js +7 -0
  116. package/dist/lib/work-context/project.d.ts +36 -10
  117. package/dist/lib/work-context/project.js +130 -41
  118. package/dist/lib/work-context/reconcile.d.ts +6 -0
  119. package/dist/lib/work-context/reconcile.js +25 -5
  120. package/dist/lib/work-context/state.d.ts +6 -0
  121. package/dist/lib/work-context/state.js +29 -1
  122. package/dist/lib/workers-registry/index.d.ts +6 -0
  123. package/dist/lib/workers-registry/index.js +8 -0
  124. package/dist/utils/copy-tree.d.ts +61 -0
  125. package/dist/utils/copy-tree.js +249 -0
  126. package/dist/utils/environmental-error.js +7 -3
  127. package/dist/utils/headless.d.ts +2 -0
  128. package/dist/utils/headless.js +8 -0
  129. package/dist/utils/login-browser.d.ts +4 -0
  130. package/dist/utils/login-browser.js +122 -0
  131. package/dist/utils/npm-invocation.d.ts +27 -0
  132. package/dist/utils/npm-invocation.js +54 -0
  133. package/dist/utils/safe-link.d.ts +50 -0
  134. package/dist/utils/safe-link.js +300 -0
  135. package/dist/utils/team-upgrade.js +1 -7
  136. package/package.json +17 -5
  137. package/scripts/hook-engine-manifest.mjs +22 -0
  138. package/scripts/install-hook-engine.mjs +10 -0
@@ -267,7 +267,7 @@ export function registerFilesCommand(program) {
267
267
  });
268
268
  files
269
269
  .command("unshare <prefix>")
270
- .description("Remove a named file access grant, or remove an empty ACL row without --with.")
270
+ .description("Remove a named file access grant, or remove an empty ACL row or grantee-less entries without --with.")
271
271
  .option("--with <principal>", "Person uid, agent uid, email address, group id, or '@all' to remove the grant")
272
272
  .action(async (prefix, opts) => {
273
273
  try {
@@ -328,17 +328,21 @@ export function registerFilesCommand(program) {
328
328
  }
329
329
  if (!res.ok) {
330
330
  if (res.status === 403) {
331
- console.error(chalk.red("Not authorized to remove this ACL row"));
332
- await offerAccessLadder(canonicalPrefix, {
333
- company: files.opts().company,
334
- });
331
+ console.error(chalk.red(body.error ?? "Not authorized to remove this ACL row"));
332
+ console.error(chalk.dim("Ask a company admin to remove this entry."));
335
333
  }
336
334
  else {
337
335
  console.error(chalk.red(body.error ?? "Unable to remove ACL row"));
338
336
  }
339
337
  process.exit(1);
340
338
  }
341
- console.log(chalk.green(`Removed empty ACL pattern '${canonicalPrefix}'. It now follows its parent folder's access, so anyone who can see that parent can see this folder.`));
339
+ const removedCount = body.removedEntries;
340
+ const cleanupMessage = typeof removedCount === "number" && removedCount > 0
341
+ ? `Removed ${removedCount} empty ${removedCount === 1 ? "entry" : "entries"} from '${canonicalPrefix}'. `
342
+ : "";
343
+ console.log(chalk.green(body.aclRemoved === false
344
+ ? `${cleanupMessage || `Removed grantee-less entries from '${canonicalPrefix}'. `}Existing grants remain.`
345
+ : `${cleanupMessage || `Removed empty ACL pattern '${canonicalPrefix}'. `}It now follows its parent folder's access, so anyone who can see that parent can see this folder.`));
342
346
  return;
343
347
  }
344
348
  const res = await vaultApiFetch({
@@ -1,6 +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 ShimPathCheck } from '../lib/windows/user-path.js';
4
5
  import { type HookTarget } from '../lib/install/claude-targets.js';
5
6
  export interface GlobalInstallOptions {
6
7
  /** HQ checkout to install from (absolute). */
@@ -28,6 +29,8 @@ export interface GlobalInstallOptions {
28
29
  marketplaceRepository?: string;
29
30
  /** Test seam for the Claude Code CLI; production uses `claude`. */
30
31
  runClaude?: (args: string[], home: string, env: NodeJS.ProcessEnv) => void;
32
+ /** Platform override for link creation (default process.platform). */
33
+ platform?: NodeJS.Platform;
31
34
  }
32
35
  /** True when an hq daemon unit (launchd plist or systemd unit) exists. */
33
36
  export declare function isDaemonInstalled(home: string, platform?: NodeJS.Platform, queryTask?: (task: string) => boolean): boolean;
@@ -58,6 +61,8 @@ export interface GlobalUninstallOptions {
58
61
  log?: (line: string) => void;
59
62
  /** Test seam for the Claude Code CLI; production uses `claude`. */
60
63
  runClaude?: (args: string[], home: string, env: NodeJS.ProcessEnv) => void;
64
+ /** Platform override for link creation (default process.platform). */
65
+ platform?: NodeJS.Platform;
61
66
  }
62
67
  /** Reverse every install step; non-HQ entries are left untouched. */
63
68
  export declare function uninstallGlobalClaude(opts?: GlobalUninstallOptions): string[];
@@ -83,6 +88,8 @@ export interface GlobalCommandOpts {
83
88
  cwd?: string;
84
89
  /** Test seam for trusting the Codex hooks a global install registers. */
85
90
  trustCodexHooks?: (cwd: string, keys: readonly string[]) => Promise<RuntimeHookTrustResult>;
91
+ /** Test seam: is the npm hq.cmd folder on the Windows PATH? Null off Windows. */
92
+ shimPathCheck?: () => ShimPathCheck | null;
86
93
  }
87
94
  export declare function resolveInstallRoot(explicit: string | undefined, options?: {
88
95
  env?: NodeJS.ProcessEnv;
@@ -17,6 +17,7 @@ import { seedRegistryFromHqRoot } from './link.js';
17
17
  import { resolveHqRoot } from './doctor.js';
18
18
  import { anywhereRuntimeEnabled, AnywhereRuntimeDisabledError } from './anywhere-runtime-flag.js';
19
19
  import { resolveHqMcpEndpoint } from '../lib/daemon/mcp-endpoint.js';
20
+ import { linkDirectory, removeLinkOnly, removeSkillEntry, skillCopySource } from '../utils/safe-link.js';
20
21
  import { chatgptConnectorInstructions } from '../lib/install/chatgpt-targets.js';
21
22
  import { anywhereRuntimeAllowed } from '../lib/anywhere-person-setting.js';
22
23
  import { grokConnectorInstructions } from '../lib/install/grok-targets.js';
@@ -28,6 +29,7 @@ import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK, SYSTEM_
28
29
  import { readDaemonConfig } from '../lib/daemon/config.js';
29
30
  import { daemonPaths } from '../lib/daemon/paths.js';
30
31
  import { assertProviderWrappersInstallable, installProviderWrappers, providerWrapperInstallPaths, removeProviderWrappers } from '../lib/install/provider-wrappers.js';
32
+ import { checkShimOnPath, shimPathWarning } from '../lib/windows/user-path.js';
31
33
  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
34
  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
35
  /** True when an hq daemon unit (launchd plist or systemd unit) exists. */
@@ -206,6 +208,22 @@ function mcpManifest(opts) {
206
208
  }
207
209
  return { type: 'stdio', command, args: [entry, 'mcp', 'serve'], env: { PATH: MCP_SUBPROCESS_PATH } };
208
210
  }
211
+ /**
212
+ * The folder an HQ skill entry stands for: a link's resolved target, or the
213
+ * source recorded in a marked copy. Null for anything else.
214
+ */
215
+ function skillEntryTarget(skillsDir, entry) {
216
+ const st = fs.lstatSync(entry);
217
+ if (st.isSymbolicLink())
218
+ return path.resolve(skillsDir, fs.readlinkSync(entry));
219
+ return skillCopySource(entry);
220
+ }
221
+ function reportSkillCopies(kinds, log) {
222
+ const copies = kinds.filter((k) => k === 'copy').length;
223
+ if (copies === 0)
224
+ return;
225
+ 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.`);
226
+ }
209
227
  function skillLinkName(name) {
210
228
  return name.startsWith(SKILL_LINK_PREFIX) ? name : `${SKILL_LINK_PREFIX}${name}`;
211
229
  }
@@ -254,7 +272,7 @@ function planSkills(hqRoot, t) {
254
272
  }
255
273
  if (!st)
256
274
  plan.link.push(name);
257
- else if (st.isSymbolicLink() && path.resolve(t.skillsDir, fs.readlinkSync(link)) === target)
275
+ else if (skillEntryTarget(t.skillsDir, link) === target)
258
276
  plan.already.push(name);
259
277
  else
260
278
  plan.skipped.push(name);
@@ -283,9 +301,8 @@ function hqSkillLinksForRoot(skillsDir, hqRoot) {
283
301
  return names.filter((name) => {
284
302
  const link = path.join(skillsDir, name);
285
303
  try {
286
- if (!fs.lstatSync(link).isSymbolicLink())
287
- return false;
288
- return path.dirname(path.resolve(skillsDir, fs.readlinkSync(link))) === hqSkillsSource(hqRoot);
304
+ const target = skillEntryTarget(skillsDir, link);
305
+ return target !== null && path.dirname(target) === hqSkillsSource(hqRoot);
289
306
  }
290
307
  catch (error) {
291
308
  if (error.code === 'ENOENT')
@@ -301,8 +318,8 @@ function hqCompanySkillLinksForRoot(skillsDir, hqRoot) {
301
318
  const source = hqSkillsSource(hqRoot);
302
319
  return hqSkillLinksForRoot(skillsDir, hqRoot).filter((link) => {
303
320
  try {
304
- const target = path.resolve(skillsDir, fs.readlinkSync(link));
305
- if (path.dirname(target) !== source)
321
+ const target = skillEntryTarget(skillsDir, link);
322
+ if (target === null || path.dirname(target) !== source)
306
323
  return false;
307
324
  const sourceName = path.basename(target);
308
325
  const separator = sourceName.indexOf(':');
@@ -492,22 +509,41 @@ export function installGlobalClaude(opts) {
492
509
  throw error;
493
510
  }
494
511
  }
495
- writeFileAtomic(t.hqPointer, pointerNew);
496
- if (mdOld !== mdNew) {
497
- if (mdNew === null) {
498
- if (fs.existsSync(t.claudeMd))
499
- fs.unlinkSync(t.claudeMd);
512
+ // Skill links go first: they are the step most likely to fail (Windows
513
+ // without symlink privilege), and a failure here must not leave ~/.hq/root
514
+ // or the CLAUDE.md block behind. On any error, undo this run's writes.
515
+ const skillsDirExisted = fs.existsSync(t.skillsDir);
516
+ const createdLinks = [];
517
+ const linkKinds = [];
518
+ try {
519
+ if (!pluginSource)
520
+ fs.mkdirSync(t.skillsDir, { recursive: true });
521
+ for (const name of skills.link) {
522
+ const link = path.join(t.skillsDir, skillLinkName(name));
523
+ linkKinds.push(linkDirectory(path.join(hqSkillsSource(hqRoot), name), link, { platform: opts.platform }));
524
+ createdLinks.push(link);
525
+ }
526
+ writeFileAtomic(t.hqPointer, pointerNew);
527
+ if (mdOld !== mdNew) {
528
+ if (mdNew === null) {
529
+ if (fs.existsSync(t.claudeMd))
530
+ fs.unlinkSync(t.claudeMd);
531
+ }
532
+ else
533
+ writeFileAtomic(t.claudeMd, mdNew);
500
534
  }
501
- else
502
- writeFileAtomic(t.claudeMd, mdNew);
503
535
  }
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)));
536
+ catch (error) {
537
+ rollbackLinkPhase({
538
+ createdLinks,
539
+ skillsDir: skillsDirExisted ? null : t.skillsDir,
540
+ files: [[t.hqPointer, pointerOld], [t.claudeMd, mdOld]],
541
+ log,
542
+ });
543
+ throw error;
508
544
  }
509
545
  for (const link of skillLinksToRemove)
510
- fs.unlinkSync(link);
546
+ removeSkillEntry(link, log);
511
547
  if (pluginSource) {
512
548
  // Migrate a prior direct install: remove only our old hook/MCP entries while
513
549
  // retaining the original user settings for a later uninstall restore.
@@ -574,6 +610,7 @@ export function installGlobalClaude(opts) {
574
610
  installProviderWrappers(providerBin, home);
575
611
  log(`HQ installed for Claude Code from ${hqRoot}.`);
576
612
  log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
613
+ reportSkillCopies(linkKinds, log);
577
614
  reportSkippedSkills(skills, log, false);
578
615
  if (pluginSource)
579
616
  log(` plugin: ${pluginSource.pluginId} installed from ${pluginSource.sourceDir}; direct hooks and MCP entries removed`);
@@ -584,6 +621,38 @@ export function installGlobalClaude(opts) {
584
621
  log(` registry: ${seeded.length} manifest entr${seeded.length === 1 ? 'y' : 'ies'} seeded`);
585
622
  return touched;
586
623
  }
624
+ /**
625
+ * Undo the link phase of a failed install: remove only the links this run
626
+ * created (never recursing into them), put each file back to its pre-run
627
+ * bytes, and drop the skills folder if this run created it and it is empty.
628
+ * A copy that is kept instead of removed is reported through `log`.
629
+ */
630
+ function rollbackLinkPhase(args) {
631
+ for (const link of [...args.createdLinks].reverse()) {
632
+ try {
633
+ removeSkillEntry(link, args.log);
634
+ }
635
+ catch { /* keep rolling back */ }
636
+ }
637
+ for (const [file, original] of args.files) {
638
+ try {
639
+ if (original === null) {
640
+ if (fs.existsSync(file))
641
+ fs.unlinkSync(file);
642
+ }
643
+ else if (readOrNull(file) !== original)
644
+ writeFileAtomic(file, original);
645
+ }
646
+ catch { /* keep rolling back */ }
647
+ }
648
+ if (args.skillsDir) {
649
+ try {
650
+ if (fs.readdirSync(args.skillsDir).length === 0)
651
+ fs.rmdirSync(args.skillsDir);
652
+ }
653
+ catch { /* keep rolling back */ }
654
+ }
655
+ }
587
656
  /** Reverse every install step; non-HQ entries are left untouched. */
588
657
  export function uninstallGlobalClaude(opts = {}) {
589
658
  const home = resolveEnv({ home: opts.home }).home;
@@ -621,11 +690,11 @@ export function uninstallGlobalClaude(opts = {}) {
621
690
  const links = [];
622
691
  try {
623
692
  for (const d of fs.readdirSync(t.skillsDir, { withFileTypes: true })) {
624
- if (!d.name.startsWith(SKILL_LINK_PREFIX) || !d.isSymbolicLink())
693
+ if (!d.name.startsWith(SKILL_LINK_PREFIX) || !(d.isSymbolicLink() || d.isDirectory()))
625
694
  continue;
626
695
  const link = path.join(t.skillsDir, d.name);
627
- const target = path.resolve(t.skillsDir, fs.readlinkSync(link));
628
- if (path.dirname(target) === source)
696
+ const target = skillEntryTarget(t.skillsDir, link);
697
+ if (target !== null && path.dirname(target) === source)
629
698
  links.push(link);
630
699
  }
631
700
  }
@@ -693,7 +762,7 @@ export function uninstallGlobalClaude(opts = {}) {
693
762
  }
694
763
  }
695
764
  for (const link of links)
696
- fs.unlinkSync(link);
765
+ removeSkillEntry(link, log);
697
766
  if (md !== null && mdNext !== md) {
698
767
  if (mdNext === null || (mdNext === '' && state?.claudeMd.original == null))
699
768
  fs.unlinkSync(t.claudeMd);
@@ -842,12 +911,34 @@ export function installGlobalCodex(opts) {
842
911
  log(lineDiff(codexConfigFormat.serialize(tomlDoc), codexConfigFormat.serialize(tomlNext)));
843
912
  return touched;
844
913
  }
845
- writeFileAtomic(hqPointer, pointerNew);
846
- writeFileAtomic(t.agentsMd, mdNew);
914
+ // Skill links go first so a link failure leaves no pointer or AGENTS.md
915
+ // block behind (see installGlobalClaude).
916
+ const skillsDirExisted = fs.existsSync(t.skillsDir);
917
+ const createdLinks = [];
918
+ const linkKinds = [];
919
+ try {
920
+ fs.mkdirSync(t.skillsDir, { recursive: true });
921
+ for (const name of skills.link) {
922
+ const link = path.join(t.skillsDir, skillLinkName(name));
923
+ linkKinds.push(linkDirectory(path.join(hqSkillsSource(hqRoot), name), link, { platform: opts.platform }));
924
+ createdLinks.push(link);
925
+ }
926
+ writeFileAtomic(hqPointer, pointerNew);
927
+ writeFileAtomic(t.agentsMd, mdNew);
928
+ }
929
+ catch (error) {
930
+ rollbackLinkPhase({
931
+ createdLinks,
932
+ skillsDir: skillsDirExisted ? null : t.skillsDir,
933
+ files: [[hqPointer, pointerOld], [t.agentsMd, mdOld]],
934
+ log,
935
+ });
936
+ throw error;
937
+ }
847
938
  // Legacy mirror: replace only when HQ-owned; record enough to put it back.
848
939
  let legacyRecord = prior?.legacy ?? { kind: 'none' };
849
940
  if (legacy.kind === 'hq-symlink') {
850
- fs.unlinkSync(legacyPath);
941
+ removeLinkOnly(legacyPath);
851
942
  legacyRecord = { kind: 'symlink', target: legacy.target };
852
943
  }
853
944
  else if (legacy.kind === 'hq-dir') {
@@ -857,12 +948,8 @@ export function installGlobalCodex(opts) {
857
948
  }
858
949
  else if (legacy.kind === 'foreign')
859
950
  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
951
  for (const link of companySkillLinksToRemove)
865
- fs.unlinkSync(link);
952
+ removeSkillEntry(link, log);
866
953
  writeConfigAtomic({
867
954
  target: t.hooksJson,
868
955
  pack: HQ_ANYWHERE_PACK,
@@ -905,6 +992,7 @@ export function installGlobalCodex(opts) {
905
992
  installProviderWrappers(providerBin, home);
906
993
  log(`HQ installed for Codex from ${hqRoot}.`);
907
994
  log(` skills linked: ${skills.link.length} new, ${skills.already.length} already present, ${skills.skipped.length} skipped`);
995
+ reportSkillCopies(linkKinds, log);
908
996
  reportSkippedSkills(skills, log, false);
909
997
  log(` hooks: ${hookTarget === 'shim' ? shimPath(hqRoot) : codexAdapterPath(hqRoot)} registered in ${t.hooksJson}`);
910
998
  log(` MCP: "${HQ_MCP_SERVER_NAME}" registered in ${t.configToml}`);
@@ -943,11 +1031,11 @@ export function uninstallGlobalCodex(opts = {}) {
943
1031
  const links = [];
944
1032
  try {
945
1033
  for (const d of fs.readdirSync(t.skillsDir, { withFileTypes: true })) {
946
- if (!d.name.startsWith(SKILL_LINK_PREFIX) || !d.isSymbolicLink())
1034
+ if (!d.name.startsWith(SKILL_LINK_PREFIX) || !(d.isSymbolicLink() || d.isDirectory()))
947
1035
  continue;
948
1036
  const link = path.join(t.skillsDir, d.name);
949
- const target = path.resolve(t.skillsDir, fs.readlinkSync(link));
950
- if (path.dirname(target) === source)
1037
+ const target = skillEntryTarget(t.skillsDir, link);
1038
+ if (target !== null && path.dirname(target) === source)
951
1039
  links.push(link);
952
1040
  }
953
1041
  }
@@ -1012,10 +1100,10 @@ export function uninstallGlobalCodex(opts = {}) {
1012
1100
  }
1013
1101
  }
1014
1102
  for (const link of links)
1015
- fs.unlinkSync(link);
1103
+ removeSkillEntry(link, log);
1016
1104
  if (restoreLegacy && state) {
1017
1105
  if (state.legacy.kind === 'symlink')
1018
- fs.symlinkSync(state.legacy.target, legacyPath);
1106
+ linkDirectory(state.legacy.target, legacyPath, { platform: opts.platform, allowCopy: false });
1019
1107
  else if (state.legacy.kind === 'dir' && fs.existsSync(state.legacy.backup))
1020
1108
  fs.renameSync(state.legacy.backup, legacyPath);
1021
1109
  }
@@ -1124,6 +1212,12 @@ export async function runInstallGlobal(opts) {
1124
1212
  });
1125
1213
  if (runtime === 'codex' && !opts.dryRun)
1126
1214
  await trustInstalledCodexHooks(hqRoot, opts);
1215
+ if (!opts.dryRun) {
1216
+ const shimPath = (opts.shimPathCheck ?? (() => checkShimOnPath({ env })))();
1217
+ if (shimPath)
1218
+ for (const line of shimPathWarning(shimPath))
1219
+ (opts.log ?? console.log)(line);
1220
+ }
1127
1221
  }
1128
1222
  /**
1129
1223
  * 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