@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
@@ -12,6 +12,7 @@ import { FLAG_REGISTRY_WORKER_SOURCE } from "./flag-registry-worker-source.gener
12
12
  import { resolveFlagSnapshotCompanyUid, resolveProcessFlagCompanyContext } from "./process-flag-company-context.js";
13
13
  import { registerFlagRegistryRefreshWaiter } from "./flag-registry-refresh-wait.js";
14
14
  import { flagSnapshotWriteFailureTags, readDefaultOnFlagSnapshot, readFlagSnapshotFile, writePersonFlagSnapshotFile, writeFlagSnapshotFile, } from "./flag-snapshot-file.js";
15
+ import { freshAnywhereRuntimeFlags } from "./anywhere-runtime-flag-key.js";
15
16
  function captureFlagRegistryException(error, tags, sentry) {
16
17
  if (!sentry)
17
18
  return;
@@ -42,6 +43,7 @@ function normalizedCompanyUid(value) {
42
43
  return COMPANY_UID_RE.test(candidate) ? candidate : undefined;
43
44
  }
44
45
  let heldSnapshot = null;
46
+ let heldSnapshotFetchedAt = 0;
45
47
  let processReader = null;
46
48
  let refreshCompletion = Promise.resolve();
47
49
  let completeRefresh = null;
@@ -113,7 +115,10 @@ function localOverrideOrFallback(flagKey, legacyFallback, env = process.env) {
113
115
  return processLocalOverride(flagKey, env) ?? legacyFallback();
114
116
  }
115
117
  /** Create the same snapshot-backed reader used by process gates. */
116
- export function createProcessSnapshotReader(companyIdentifiers, getSnapshot = () => heldSnapshot, env = process.env) {
118
+ export function createProcessSnapshotReader(companyIdentifiers, getSnapshot = () => heldSnapshot && ({
119
+ ...heldSnapshot,
120
+ flags: freshAnywhereRuntimeFlags(heldSnapshot.flags, heldSnapshotFetchedAt),
121
+ }), env = process.env) {
117
122
  return {
118
123
  isEnabled(flagKey, lookup = {}) {
119
124
  return resolveFlag({
@@ -169,6 +174,7 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
169
174
  : null;
170
175
  if (persistedSnapshot) {
171
176
  heldSnapshot = { version: 0, flags: persistedSnapshot.flags };
177
+ heldSnapshotFetchedAt = Date.parse(persistedSnapshot.fetched_at);
172
178
  }
173
179
  refreshCompletion = Promise.resolve();
174
180
  completeRefresh = null;
@@ -234,6 +240,7 @@ export function kickFlagRegistryReadiness(dependencies = {}) {
234
240
  worker.on("message", (value) => {
235
241
  if (isFlagSnapshot(value)) {
236
242
  heldSnapshot = value;
243
+ heldSnapshotFetchedAt = Date.now();
237
244
  if (completeRefresh)
238
245
  refreshPendingSnapshot = value;
239
246
  if (refreshCompanyUid && refreshPendingSnapshot) {
@@ -392,6 +399,7 @@ export function currentFlagSnapshot() {
392
399
  /** Test-only reset for process-global readiness state. */
393
400
  export function _resetFlagRegistryForTests() {
394
401
  heldSnapshot = null;
402
+ heldSnapshotFetchedAt = 0;
395
403
  processReader = null;
396
404
  refreshCompletion = Promise.resolve();
397
405
  completeRefresh = null;
@@ -2,6 +2,7 @@
2
2
  import * as fs from "node:fs";
3
3
  import * as os from "node:os";
4
4
  import * as path from "node:path";
5
+ import { freshAnywhereRuntimeFlags, HQ_ANYWHERE_RUNTIME_FLAG, HQ_ANYWHERE_RUNTIME_FALSE_MAX_AGE_MS } from "./anywhere-runtime-flag-key.js";
5
6
  export const FLAG_SNAPSHOT_MAX_AGE_MS = 24 * 60 * 60 * 1000;
6
7
  const COMPANY_UID_RE = /^cmp_[A-Za-z0-9]{3,128}$/;
7
8
  function markWriteFailure(error, context) {
@@ -263,7 +264,7 @@ export function readPersonFlagSnapshotFile(options = {}) {
263
264
  continue;
264
265
  if (file === fallback && fallback)
265
266
  fallbackCacheUsed = true;
266
- return { scope: "person", fetched_at: value.fetched_at, flags: flags };
267
+ return { scope: "person", fetched_at: value.fetched_at, flags: freshAnywhereRuntimeFlags(flags, fetchedAt, options.now) };
267
268
  }
268
269
  return null;
269
270
  }
@@ -348,7 +349,7 @@ function parseSnapshotFile(companyUid, file, options) {
348
349
  return {
349
350
  company_uid: companyUid,
350
351
  fetched_at: value.fetched_at,
351
- flags: flags,
352
+ flags: freshAnywhereRuntimeFlags(flags, fetchedAt, options.now),
352
353
  };
353
354
  }
354
355
  /** Return null for every miss, malformed file, company mismatch, or stale file. */
@@ -374,7 +375,11 @@ export function readFlagSnapshotFile(companyUid, options = {}) {
374
375
  }
375
376
  /** Resolve a default-on kill switch from offline snapshots, allowing stale decisions. */
376
377
  export function readDefaultOnFlagSnapshot(flagKey, companyUid, env = process.env) {
377
- const options = { env, maxAgeMs: Number.POSITIVE_INFINITY };
378
+ // Native-route kill switches intentionally retain stale decisions. Only
379
+ // Anywhere has the GA one-hour disable limit; do not change other gates.
380
+ const options = { env, maxAgeMs: flagKey === HQ_ANYWHERE_RUNTIME_FLAG
381
+ ? HQ_ANYWHERE_RUNTIME_FALSE_MAX_AGE_MS
382
+ : Number.POSITIVE_INFINITY };
378
383
  if (companyUid)
379
384
  return readFlagSnapshotFile(companyUid, options)?.flags[flagKey] !== false;
380
385
  const directories = new Set([path.dirname(personFlagSnapshotPath(env))]);
@@ -0,0 +1,31 @@
1
+ export declare const HOOK_ENGINE_REPAIR = "npm install -g @indigoai-us/hq-cli --include=optional";
2
+ export declare const HOOK_ENGINE_REPAIR_PIN = "hq core hook-engine-path --repair";
3
+ export interface EngineOptions {
4
+ home?: string;
5
+ platform?: string;
6
+ arch?: string;
7
+ version?: string;
8
+ resolvePackage?: (name: string) => string;
9
+ repair?: boolean;
10
+ }
11
+ export interface EnginePackage {
12
+ source: string;
13
+ pinned: string;
14
+ version: string;
15
+ sourceDirectory: string;
16
+ pinnedDirectory: string;
17
+ }
18
+ /** US-020 must set this package capability when the replaced bash hooks retire. */
19
+ export declare function hookEngineRequired(): boolean;
20
+ /** Check the whole signed bundle, including its resource seal and stapled ticket. */
21
+ export declare function pinnedHookEngineMatches(engine: EnginePackage): boolean;
22
+ /** Offline lookup. Never silently use an engine from a different CLI version. */
23
+ export declare function findHookEngine(options?: EngineOptions): EnginePackage | null;
24
+ /** Never truncate or replace a running Windows executable. */
25
+ export declare function installHookEngine(options?: EngineOptions): string | null;
26
+ /** Call only after settings no longer reference these old versions. Windows
27
+ * refuses unlink of a mapped executable; a busy version stays for a later pass.
28
+ * POSIX permits unlink while running, so retain those versions conservatively.
29
+ */
30
+ export declare function pruneHookEngines(current: string, referencedVersions: readonly string[], options?: EngineOptions): string[];
31
+ //# sourceMappingURL=resolve.d.ts.map
@@ -0,0 +1,134 @@
1
+ import { createRequire } from 'node:module';
2
+ import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, renameSync, rmSync, rmdirSync, unlinkSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ const require = createRequire(import.meta.url);
6
+ // Capture metadata while the package-use lease protects this install tree.
7
+ // Resident commands may call doctor after an update removes the old manifest.
8
+ const cliManifest = JSON.parse(readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'));
9
+ export const HOOK_ENGINE_REPAIR = 'npm install -g @indigoai-us/hq-cli --include=optional';
10
+ export const HOOK_ENGINE_REPAIR_PIN = 'hq core hook-engine-path --repair';
11
+ const platforms = new Set(['win32-x64', 'win32-arm64', 'darwin-x64', 'darwin-arm64', 'linux-x64', 'linux-arm64']);
12
+ /** US-020 must set this package capability when the replaced bash hooks retire. */
13
+ export function hookEngineRequired() {
14
+ return cliManifest.hqHookEngineRequired === true;
15
+ }
16
+ /** Check the whole signed bundle, including its resource seal and stapled ticket. */
17
+ export function pinnedHookEngineMatches(engine) {
18
+ function equal(source, pinned) {
19
+ if (!existsSync(pinned))
20
+ return false;
21
+ const a = lstatSync(source);
22
+ const b = lstatSync(pinned);
23
+ if (a.isSymbolicLink())
24
+ return b.isSymbolicLink() && readlinkSync(source) === readlinkSync(pinned);
25
+ if (a.isDirectory()) {
26
+ if (!b.isDirectory())
27
+ return false;
28
+ const names = readdirSync(source).sort();
29
+ if (JSON.stringify(names) !== JSON.stringify(readdirSync(pinned).sort()))
30
+ return false;
31
+ return names.every(name => equal(join(source, name), join(pinned, name)));
32
+ }
33
+ return a.isFile() && b.isFile() && readFileSync(source).equals(readFileSync(pinned));
34
+ }
35
+ return equal(engine.sourceDirectory, engine.pinnedDirectory);
36
+ }
37
+ /** Offline lookup. Never silently use an engine from a different CLI version. */
38
+ export function findHookEngine(options = {}) {
39
+ const platform = options.platform ?? process.platform;
40
+ const key = `${platform}-${options.arch ?? process.arch}`;
41
+ if (!platforms.has(key))
42
+ return null;
43
+ const version = options.version ?? cliManifest.version;
44
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version))
45
+ throw new Error('Invalid hook engine version');
46
+ let manifest;
47
+ try {
48
+ manifest = (options.resolvePackage ?? require.resolve)(`@indigoai-us/hq-hook-${key}/package.json`);
49
+ }
50
+ catch (error) {
51
+ if (error.code === 'MODULE_NOT_FOUND')
52
+ return null;
53
+ throw error;
54
+ }
55
+ const pkg = JSON.parse(readFileSync(manifest, 'utf8'));
56
+ if (pkg.version !== version)
57
+ return null;
58
+ const binary = platform === 'win32' ? 'hq-hook.exe' : platform === 'darwin' ? join('hq-hook.app', 'Contents', 'MacOS', 'hq-hook') : 'hq-hook';
59
+ const sourceDirectory = join(dirname(manifest), 'bin');
60
+ const pinnedDirectory = join(options.home ?? homedir(), '.hq', 'bin', 'hq-hook', version);
61
+ const source = join(sourceDirectory, binary);
62
+ if (!existsSync(source))
63
+ return null;
64
+ return { source, version, sourceDirectory, pinnedDirectory, pinned: join(pinnedDirectory, binary) };
65
+ }
66
+ /** Never truncate or replace a running Windows executable. */
67
+ export function installHookEngine(options = {}) {
68
+ const engine = findHookEngine(options);
69
+ if (!engine)
70
+ return null;
71
+ if (options.repair && existsSync(engine.pinnedDirectory)) {
72
+ // Unlink the executable first: Windows refuses this if any process maps it.
73
+ // Never recursively remove the bundle before that lock check has succeeded.
74
+ try {
75
+ unlinkSync(engine.pinned);
76
+ }
77
+ catch (error) {
78
+ if (error.code !== 'ENOENT')
79
+ throw new Error('Close running HQ hook processes before repairing the pinned engine', { cause: error });
80
+ }
81
+ rmSync(engine.pinnedDirectory, { recursive: true });
82
+ }
83
+ mkdirSync(dirname(engine.pinnedDirectory), { recursive: true });
84
+ if (!existsSync(engine.pinnedDirectory)) {
85
+ const stage = mkdtempSync(join(dirname(engine.pinnedDirectory), '.install-'));
86
+ try {
87
+ cpSync(engine.sourceDirectory, stage, { recursive: true, errorOnExist: true, force: false });
88
+ try {
89
+ renameSync(stage, engine.pinnedDirectory);
90
+ }
91
+ catch (error) {
92
+ if (!existsSync(engine.pinned))
93
+ throw error;
94
+ }
95
+ }
96
+ finally {
97
+ rmSync(stage, { recursive: true, force: true });
98
+ }
99
+ }
100
+ if (!pinnedHookEngineMatches(engine)) {
101
+ throw new Error(`Hook engine ${engine.version} differs from its pinned copy; close HQ sessions and repair with: ${HOOK_ENGINE_REPAIR_PIN}`);
102
+ }
103
+ return engine.pinned;
104
+ }
105
+ /** Call only after settings no longer reference these old versions. Windows
106
+ * refuses unlink of a mapped executable; a busy version stays for a later pass.
107
+ * POSIX permits unlink while running, so retain those versions conservatively.
108
+ */
109
+ export function pruneHookEngines(current, referencedVersions, options = {}) {
110
+ if ((options.platform ?? process.platform) !== 'win32')
111
+ return [];
112
+ const root = join(options.home ?? homedir(), '.hq', 'bin', 'hq-hook');
113
+ if (!existsSync(root))
114
+ return [];
115
+ const removed = [];
116
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
117
+ if (!entry.isDirectory() || entry.name === current || referencedVersions.includes(entry.name) || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(entry.name))
118
+ continue;
119
+ const dir = join(root, entry.name);
120
+ if (readdirSync(dir).some(name => name !== 'hq-hook.exe'))
121
+ continue;
122
+ try {
123
+ unlinkSync(join(dir, 'hq-hook.exe'));
124
+ rmdirSync(dir);
125
+ removed.push(entry.name);
126
+ }
127
+ catch (error) {
128
+ if (!['EPERM', 'EACCES', 'EBUSY', 'ENOENT', 'ENOTEMPTY'].includes(error.code ?? ''))
129
+ throw error;
130
+ }
131
+ }
132
+ return removed;
133
+ }
134
+ //# sourceMappingURL=resolve.js.map
@@ -13,6 +13,7 @@ export type OutpostIdentity = {
13
13
  export declare function readOutpostIdentity(existsSync?: (p: string) => boolean, readFileSync?: (p: string) => string): OutpostIdentity;
14
14
  export declare function resolveAdmissionHost(deps?: {
15
15
  hostname?: () => string;
16
+ homedir?: () => string;
16
17
  readOutpostIdentity?: () => OutpostIdentity;
17
18
  existsSync?: (p: string) => boolean;
18
19
  readFileSync?: (p: string) => string;
@@ -1,5 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
+ import { observeLocalHostIdentity } from "./local-host-identity.js";
3
4
  export const OUTPOST_ID_PATH = "/etc/outpost/outpost-id";
4
5
  export const OUTPOST_DIR = "/etc/outpost";
5
6
  export function readOutpostIdentity(existsSync = (p) => fs.existsSync(p), readFileSync = (p) => fs.readFileSync(p, "utf8")) {
@@ -25,9 +26,15 @@ export function readOutpostIdentity(existsSync = (p) => fs.existsSync(p), readFi
25
26
  }
26
27
  export function resolveAdmissionHost(deps = {}) {
27
28
  const identity = deps.readOutpostIdentity?.() ?? readOutpostIdentity(deps.existsSync, deps.readFileSync);
29
+ const hostname = (deps.hostname ?? os.hostname)();
30
+ const observed = observeLocalHostIdentity({
31
+ homedir: deps.homedir?.() ?? os.homedir(),
32
+ hostname,
33
+ });
28
34
  return {
29
- hostname: (deps.hostname ?? os.hostname)(),
35
+ hostname,
30
36
  outpostId: identity.kind === "id" ? identity.id : null,
37
+ ...(observed ? { machineId: observed.id, aliases: observed.aliases } : {}),
31
38
  };
32
39
  }
33
40
  /** `primary` names the controller registry unless the caller is a different Outpost. */
@@ -3,6 +3,10 @@ import { type LaneRecord, type LanesConfig } from "./types.js";
3
3
  export interface AdmissionHost {
4
4
  hostname: string;
5
5
  outpostId: string | null;
6
+ /** Stable id from ~/.hq/lane-host.json, when this process has read one. */
7
+ machineId?: string;
8
+ /** Hostnames this machine has already used. */
9
+ aliases?: readonly string[];
6
10
  }
7
11
  export type AdmissionCapReason = "provider_cap" | "account_cap" | "profile_cap";
8
12
  export declare const ADMISSION_LOCK_STALE_MS = 30000;
@@ -2,8 +2,9 @@ import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { accountMaxRunning } from "./config.js";
4
4
  import { LanesError } from "./errors.js";
5
+ import { localHostIdsReferToSameMachine } from "./local-host-identity.js";
5
6
  import { quotaBlockFor } from "./quota.js";
6
- import { isTerminalLaneState } from "./types.js";
7
+ import { isSubagentLane, isTerminalLaneState } from "./types.js";
7
8
  export const ADMISSION_LOCK_STALE_MS = 30_000;
8
9
  const ADMISSION_LOCK_TIMEOUT_MS = 5_000;
9
10
  const ADMISSION_RESERVATION_GRACE_MS = 30_000;
@@ -117,7 +118,7 @@ export function laneRunsOnHost(lane, host) {
117
118
  if (host.outpostId && lane.host.kind === "outpost") {
118
119
  return lane.host.id === host.outpostId;
119
120
  }
120
- return lane.host.kind === "local" && lane.host.id === host.hostname;
121
+ return lane.host.kind === "local" && localHostIdsReferToSameMachine(lane.host.id, host);
121
122
  }
122
123
  export function compareLaneQueueOrder(a, b) {
123
124
  if (b.priority !== a.priority)
@@ -188,6 +189,9 @@ export function laneOccupiesRunningSlot(lane, nowMs = Date.now()) {
188
189
  // count as a second owner of the slot that blocked it.
189
190
  if (lane.state === "queued" && lane.queued_reason === "account_cap")
190
191
  return false;
192
+ // A subagent lane runs inside its senior session, not in an HQ provider slot.
193
+ if (isSubagentLane(lane))
194
+ return false;
191
195
  return lane.state === "running" ||
192
196
  hasActiveAdmissionReservation(lane, nowMs) ||
193
197
  hasActivePendingResume(lane, nowMs);
@@ -17,6 +17,11 @@ export interface EnvelopeRepairDispatchDeps {
17
17
  env?: NodeJS.ProcessEnv;
18
18
  spawnSync?: (command: string, args: string[], options: EnvelopeRepairDispatchOptions) => EnvelopeRepairDispatchResult;
19
19
  }
20
+ /**
21
+ * True for the detached lane runner scripts. They are not the hq CLI: running
22
+ * one with `lanes ...` argv starts that runner for a lane named "lanes".
23
+ */
24
+ export declare function isLaneRunnerEntry(entry: string): boolean;
20
25
  /**
21
26
  * Dispatch repair into the currently installed CLI. A watcher can stay alive
22
27
  * across upgrades, so repair must not call its in-memory spawn implementation.
@@ -1,6 +1,14 @@
1
1
  import { spawnSync as nodeSpawnSync } from "node:child_process";
2
2
  import * as path from "node:path";
3
3
  import { LanesError } from "./errors.js";
4
+ const LANE_RUNNER_ENTRY_NAMES = new Set(["watcher-main", "worker-process", "remote-control-lane"]);
5
+ /**
6
+ * True for the detached lane runner scripts. They are not the hq CLI: running
7
+ * one with `lanes ...` argv starts that runner for a lane named "lanes".
8
+ */
9
+ export function isLaneRunnerEntry(entry) {
10
+ return LANE_RUNNER_ENTRY_NAMES.has(path.basename(entry).replace(/\.[cm]?[jt]s$/, ""));
11
+ }
4
12
  /**
5
13
  * Dispatch repair into the currently installed CLI. A watcher can stay alive
6
14
  * across upgrades, so repair must not call its in-memory spawn implementation.
@@ -28,7 +36,10 @@ export function spawnEnvelopeRepairViaCurrentCli(hqRoot, lane, input, deps = {})
28
36
  });
29
37
  const env = deps.env ?? process.env;
30
38
  const cliEntry = env.HQ_LANE_CLI_ENTRY?.trim();
31
- if (!cliEntry || !path.isAbsolute(cliEntry) || path.resolve(cliEntry) !== cliEntry) {
39
+ if (!cliEntry ||
40
+ !path.isAbsolute(cliEntry) ||
41
+ path.resolve(cliEntry) !== cliEntry ||
42
+ isLaneRunnerEntry(cliEntry)) {
32
43
  throw new LanesError("repair_cli_entry_invalid", "Lane watcher has no valid absolute hq CLI entrypoint for envelope repair", { lane_id: lane.lane_id });
33
44
  }
34
45
  const result = run(process.execPath, [cliEntry, "lanes", "_spawn-envelope-repair"], {
@@ -36,8 +36,8 @@ export { ENVELOPE_INVALID_REASON, decideEnvelopeRepair, emitMeshSoft, envelopeQu
36
36
  export type { EnvelopeRepairDecision, LatestEnvelopeFile } from "./watcher.js";
37
37
  export { LANE_MESH_EVENTS, WIRE_EVENT_KINDS, createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, laneCanEmit, parseLaneMeshEvent, progressEventDue, toWireThreadEvent, } from "./mesh-events.js";
38
38
  export type { CreateLaneMeshOptions, LaneMesh, LaneMeshEvent, WireEventKind, WireThreadEvent, } from "./mesh-events.js";
39
- export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, VM_STAT_BIN, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, parseMemAvailableKb, parseVmStatAvailableMb, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
40
- export type { CpuTimes, ThrottleAdmitReason, ThrottleDecision, ThrottleDeferReason, ThrottleDeps, ThrottleState, } from "./throttle.js";
39
+ export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, MEMORY_PRESSURE_SYSCTLS, SYSCTL_BIN, VM_STAT_BIN, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, formatMemoryHoldMessage, isCpuCheckEnabled, parseMemAvailableKb, parseMemoryPressureSysctl, parseVmStatAvailableMb, readAvailableMemMb, readMemorySignal, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
40
+ export type { CpuTimes, MemoryHold, MemoryPressureLevel, MemorySignal, MemorySignalSource, ThrottleAdmitReason, ThrottleDecision, ThrottleDeferReason, ThrottleDeps, ThrottleState, } from "./throttle.js";
41
41
  export { effectiveAccountHome, } from "./account-home.js";
42
42
  export type { EffectiveAccountHome, } from "./account-home.js";
43
43
  export { AUTH_JSON_NAME, CLAUDE_CREDENTIALS_NAME, PROVIDER_CREDENTIAL_FILES, PROVIDER_HOME_ENV, accountEnv, accountStatus, authJsonPath, authStateFor, claudeIdentityDiffers, claudeIdentityFromProbe, clearQuotaCommand, credentialPaths, describeClaudeIdentity, hasCredentialFile, isCredentialProven, isDefaultProviderHome, isLoggedOut, probeInteractiveClaudeIdentity, withProviderHomeEnv, isOccupyingLaneState, lastStartedAt, listAccountStatuses, occupyingLaneCount, resolveAccount, runningLaneCount, selectBalancedAccount, } from "./accounts.js";
@@ -20,7 +20,7 @@ export { PID_LIVENESS_DECISIONS, PROVIDER_COMMAND, PROVIDER_MODEL_FLAG, SPAWN_TE
20
20
  export { SILENCE_MS, WATCHER_DECISION_EVENTS, WATCHER_OUT_MAX_BYTES, WATCHER_POLL_MS, WATCHER_SIGNAL_KINDS, appendCappedLog, appendWatcherSignal, applyTerminalToLane, classifyLaneExit, clearLaneStalled, isPidAlive, laneIdentityFromRecord, markLaneStalled, notifySlotFreed, parseWatcherSignalKind, readLatestEnvelope, runWatcherLoop, stalledReason, tickWatcher, watcherEventsPath, watcherLastSignalPath, watcherOutLogPath, writeWatcherDecision, } from "./watcher.js";
21
21
  export { ENVELOPE_INVALID_REASON, decideEnvelopeRepair, emitMeshSoft, envelopeQuestionIds, readLatestEnvelopeFile, readLatestEnvelopeRecord, } from "./watcher.js";
22
22
  export { LANE_MESH_EVENTS, WIRE_EVENT_KINDS, createLaneMesh, emitLaneDecisionEvents, emitLaneProgressEvent, laneCanEmit, parseLaneMeshEvent, progressEventDue, toWireThreadEvent, } from "./mesh-events.js";
23
- export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, VM_STAT_BIN, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, isCpuCheckEnabled, parseMemAvailableKb, parseVmStatAvailableMb, readAvailableMemMb, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
23
+ export { CPU_SAMPLE_MS, LANES_CPU_BUSY_OVERRIDE, LANES_CPU_CHECK, PROC_MEMINFO, STARVATION_MAX_DEFERRALS, STARVATION_MAX_MS, MEMORY_PRESSURE_SYSCTLS, SYSCTL_BIN, VM_STAT_BIN, applyThrottleDecision, busyFractionFromDeltas, cpuTimesSnapshot, decideAdmission, defaultThrottleDeps, emptyThrottleState, evaluateThrottle, formatMemoryHoldMessage, isCpuCheckEnabled, parseMemAvailableKb, parseMemoryPressureSysctl, parseVmStatAvailableMb, readAvailableMemMb, readMemorySignal, readThrottleState, resolveCpuBusyFraction, sampleCpuBusyFraction, starvationBoundReached, throttleStatePath, writeThrottleState, } from "./throttle.js";
24
24
  export { effectiveAccountHome, } from "./account-home.js";
25
25
  export { AUTH_JSON_NAME, CLAUDE_CREDENTIALS_NAME, PROVIDER_CREDENTIAL_FILES, PROVIDER_HOME_ENV, accountEnv, accountStatus, authJsonPath, authStateFor, claudeIdentityDiffers, claudeIdentityFromProbe, clearQuotaCommand, credentialPaths, describeClaudeIdentity, hasCredentialFile, isCredentialProven, isDefaultProviderHome, isLoggedOut, probeInteractiveClaudeIdentity, withProviderHomeEnv, isOccupyingLaneState, lastStartedAt, listAccountStatuses, occupyingLaneCount, resolveAccount, runningLaneCount, selectBalancedAccount, } from "./accounts.js";
26
26
  export { LIVE_WITHIN_MS, LIVENESS_EVENT_KINDS, SILENT_LOST_MS, TURN_START_LOST_MS, buildLivenessIndexFromSpool, classifySessionLiveness, defaultLivenessDeps, defaultSpoolPath, ingestSpoolLine, listSessionLiveness, livenessIndexPath, loadLivenessIndex, readDaemonPresenceConnected, readLivenessIndex, rebuildLivenessIndex, sessionLiveness, } from "./liveness.js";
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import { LanesError } from "./errors.js";
4
4
  import { dropboxInboxHasMessages } from "./dropbox.js";
5
5
  import { appendCappedHistory, listQuestionIds, readQuestion, retryUpdateLane } from "./store.js";
6
- import { isTerminalLaneState, isIsoDateTime, } from "./types.js";
6
+ import { isSubagentLane, isTerminalLaneState, isIsoDateTime, } from "./types.js";
7
7
  export const LEAD_LANE_KEEP_ALIVE_FLAG = "lanes.lead-keep-alive";
8
8
  export const LEAD_LANE_KEEP_ALIVE_DEFAULT_ENABLED = false;
9
9
  export const LEAD_LANE_KEEP_ALIVE_NEXT_CHECK_MAX_DELAY_MS = 24 * 60 * 60 * 1000;
@@ -24,6 +24,9 @@ export const LEAD_LANE_KEEP_ALIVE_BASE_BACKOFF_MS = 5 * 60 * 1000;
24
24
  const LEAD_LANE_KEEP_ALIVE_MAX_BACKOFF_MS = 60 * 60 * 1000;
25
25
  /** Shared resume transition for terminal gate follow-ups. */
26
26
  export function queueLaneTerminalResume(current, at, resumeText, options = {}) {
27
+ // HQ holds no provider session for a subagent lane, so it can never resume one.
28
+ if (isSubagentLane(current))
29
+ return { lane: current, resumeQueued: false, resumeSkippedReason: "no_provider_session" };
27
30
  if (options.mergeExisting && current.pending_resume !== undefined) {
28
31
  return {
29
32
  lane: {
@@ -0,0 +1,40 @@
1
+ export declare const LANE_HOST_SCHEMA = "hq-lane-host.v1";
2
+ export declare const LANE_HOST_FILENAME = "lane-host.json";
3
+ export interface LocalHostIdentity {
4
+ schema_version: typeof LANE_HOST_SCHEMA;
5
+ id: string;
6
+ aliases: string[];
7
+ /** Hardware UUID, `/etc/machine-id`, or Windows MachineGuid. Absent on older files. */
8
+ hardware_id?: string;
9
+ }
10
+ export interface LocalHostMatch {
11
+ hostname: string;
12
+ machineId?: string;
13
+ aliases?: readonly string[];
14
+ }
15
+ export declare function laneHostIdentityPath(homedir: string): string;
16
+ /** True when `recordedId` is this machine's stable id, current hostname, or a known earlier hostname. */
17
+ export declare function localHostIdsReferToSameMachine(recordedId: string, host: LocalHostMatch): boolean;
18
+ /**
19
+ * Read the stable id, creating it once. A lost race re-reads the winner
20
+ * instead of minting a second id that would strand the lanes just created.
21
+ * A stored hardware id that does not match this machine mints a new id so a
22
+ * cloned home directory does not keep the source machine's id.
23
+ */
24
+ export declare function resolveLocalHostIdentity(input: {
25
+ homedir: string;
26
+ hostname: string;
27
+ createId?: () => string;
28
+ readHardwareId?: () => string | null;
29
+ }): LocalHostIdentity;
30
+ /**
31
+ * Read an existing identity. Does not create the file, record a hostname,
32
+ * or replace an id. A hardware mismatch returns null so a cloned id is not
33
+ * treated as this machine.
34
+ */
35
+ export declare function observeLocalHostIdentity(input: {
36
+ homedir: string;
37
+ hostname: string;
38
+ readHardwareId?: () => string | null;
39
+ }): LocalHostIdentity | null;
40
+ //# sourceMappingURL=local-host-identity.d.ts.map