@indigoai-us/hq-cli 5.322.0 → 5.324.0

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 (90) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/assets/scaffold/core/scripts/derive-trigger-facts.sh +263 -0
  3. package/assets/scaffold/core/scripts/lib/transcript-tail.sh +98 -0
  4. package/assets/scaffold/core/scripts/lib/trigger-fact-text.awk +86 -0
  5. package/dist/bin/hq-auth-refresh.d.ts +5 -1
  6. package/dist/bin/hq-auth-refresh.js +33 -1
  7. package/dist/command-catalog.generated.d.ts +138 -6
  8. package/dist/command-catalog.generated.js +178 -6
  9. package/dist/command-registration-plan.d.ts +24 -0
  10. package/dist/command-registration-plan.js +4 -0
  11. package/dist/commands/anywhere-runtime-flag.d.ts +9 -0
  12. package/dist/commands/anywhere-runtime-flag.js +22 -0
  13. package/dist/commands/cloud.js +23 -4
  14. package/dist/commands/core-derive-trigger-facts-command.d.ts +3 -0
  15. package/dist/commands/core-derive-trigger-facts-command.js +47 -0
  16. package/dist/commands/core-derive-trigger-facts-native.d.ts +21 -0
  17. package/dist/commands/core-derive-trigger-facts-native.js +492 -0
  18. package/dist/commands/core.js +6 -0
  19. package/dist/commands/daemon.js +39 -2
  20. package/dist/commands/db-provision.js +6 -1
  21. package/dist/commands/doctor.js +8 -4
  22. package/dist/commands/feedback.d.ts +2 -0
  23. package/dist/commands/feedback.js +10 -0
  24. package/dist/commands/install-global.d.ts +46 -0
  25. package/dist/commands/install-global.js +674 -0
  26. package/dist/commands/link.d.ts +30 -0
  27. package/dist/commands/link.js +184 -0
  28. package/dist/commands/mcp-status.d.ts +6 -0
  29. package/dist/commands/mcp-status.js +21 -4
  30. package/dist/commands/pkg-install.js +20 -1
  31. package/dist/commands/update.d.ts +19 -0
  32. package/dist/commands/update.js +52 -6
  33. package/dist/fast-core.d.ts +2 -1
  34. package/dist/fast-core.js +12 -8
  35. package/dist/fast-scaffold.js +13 -0
  36. package/dist/index.js +9 -7
  37. package/dist/lib/cli-invite-nudge.d.ts +25 -0
  38. package/dist/lib/cli-invite-nudge.js +152 -0
  39. package/dist/lib/core-utils/timeout-guard-native-flags.d.ts +6 -0
  40. package/dist/lib/core-utils/timeout-guard-native-flags.js +30 -0
  41. package/dist/lib/core-utils/timeout-guard-native.d.ts +0 -3
  42. package/dist/lib/core-utils/timeout-guard-native.js +4 -30
  43. package/dist/lib/core-utils/timeout-guard.d.ts +2 -0
  44. package/dist/lib/core-utils/timeout-guard.js +2 -1
  45. package/dist/lib/daemon/convergence.d.ts +42 -0
  46. package/dist/lib/daemon/convergence.js +66 -0
  47. package/dist/lib/daemon/heartbeat.d.ts +43 -0
  48. package/dist/lib/daemon/heartbeat.js +70 -0
  49. package/dist/lib/daemon/ops/journal.d.ts +29 -0
  50. package/dist/lib/daemon/ops/journal.js +141 -0
  51. package/dist/lib/daemon/ops/memory.d.ts +20 -0
  52. package/dist/lib/daemon/ops/memory.js +65 -0
  53. package/dist/lib/daemon/ops/mesh.d.ts +38 -0
  54. package/dist/lib/daemon/ops/mesh.js +70 -0
  55. package/dist/lib/daemon/ops/policy.d.ts +78 -0
  56. package/dist/lib/daemon/ops/policy.js +283 -0
  57. package/dist/lib/daemon/protocol.d.ts +68 -0
  58. package/dist/lib/daemon/protocol.js +154 -0
  59. package/dist/lib/daemon/run.d.ts +10 -0
  60. package/dist/lib/daemon/run.js +85 -3
  61. package/dist/lib/daemon/server.d.ts +36 -0
  62. package/dist/lib/daemon/server.js +306 -0
  63. package/dist/lib/daemon/session-registry.d.ts +30 -0
  64. package/dist/lib/daemon/session-registry.js +42 -0
  65. package/dist/lib/doctor/checks/anywhere.d.ts +38 -0
  66. package/dist/lib/doctor/checks/anywhere.js +182 -0
  67. package/dist/lib/doctor/registry.js +6 -0
  68. package/dist/lib/first-sync-next-step.d.ts +10 -1
  69. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  70. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  71. package/dist/lib/install/claude-targets.d.ts +47 -0
  72. package/dist/lib/install/claude-targets.js +203 -0
  73. package/dist/lib/install/codex-targets.d.ts +57 -0
  74. package/dist/lib/install/codex-targets.js +187 -0
  75. package/dist/lib/mesh/live/session-seq.js +62 -9
  76. package/dist/lib/registry/normalize-remote.d.ts +21 -0
  77. package/dist/lib/registry/normalize-remote.js +68 -0
  78. package/dist/lib/registry/registry.d.ts +75 -0
  79. package/dist/lib/registry/registry.js +261 -0
  80. package/dist/main.js +3 -1
  81. package/dist/utils/cli-telemetry.d.ts +11 -0
  82. package/dist/utils/cli-telemetry.js +76 -0
  83. package/dist/utils/cognito-session.d.ts +5 -1
  84. package/dist/utils/cognito-session.js +8 -12
  85. package/dist/utils/feedback-diagnostics.d.ts +2 -0
  86. package/dist/utils/run-bundled-script.d.ts +7 -4
  87. package/dist/utils/run-bundled-script.js +47 -12
  88. package/dist/utils/version-check.d.ts +2 -0
  89. package/dist/utils/version-check.js +7 -0
  90. package/package.json +2 -2
@@ -0,0 +1,30 @@
1
+ /**
2
+ * hq link / hq unlink / hq resolve-company — repo-to-company registry
3
+ * commands (hq-anywhere US-004). The registry lives at ~/.hq/registry.json;
4
+ * see src/lib/registry/registry.ts for the key and precedence rules.
5
+ */
6
+ import { Command } from "commander";
7
+ export interface MembershipCompany {
8
+ companyUid: string;
9
+ companySlug: string;
10
+ }
11
+ export interface LinkDeps {
12
+ /** Registry file path; tests point this at a temp dir. */
13
+ file?: string;
14
+ /** Active memberships for the signed-in user (reuses GET /membership/me). */
15
+ listMemberships?: () => Promise<MembershipCompany[]>;
16
+ now?: () => Date;
17
+ }
18
+ /** Link `dir` to `company` after confirming the caller is an active member. */
19
+ export declare function linkFolder(company: string, dir: string | undefined, deps?: LinkDeps): Promise<{
20
+ key: string;
21
+ company: string;
22
+ }>;
23
+ export declare function unlinkFolder(dir: string | undefined, deps?: LinkDeps): string[];
24
+ export declare function resolveCompany(dir: string | undefined, deps?: LinkDeps): import("../lib/registry/registry.js").ResolveResult;
25
+ /** Seed manifest-sourced entries; called by `hq install --global` (US-005). */
26
+ export declare function seedRegistryFromHqRoot(hqRoot: string, deps?: LinkDeps): import("../lib/registry/registry.js").SeedResult;
27
+ export declare function registerLinkCommand(program: Command): void;
28
+ export declare function registerUnlinkCommand(program: Command): void;
29
+ export declare function registerResolveCompanyCommand(program: Command): void;
30
+ //# sourceMappingURL=link.d.ts.map
@@ -0,0 +1,184 @@
1
+ /**
2
+ * hq link / hq unlink / hq resolve-company — repo-to-company registry
3
+ * commands (hq-anywhere US-004). The registry lives at ~/.hq/registry.json;
4
+ * see src/lib/registry/registry.ts for the key and precedence rules.
5
+ */
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+ import { Option } from "commander";
9
+ import chalk from "chalk";
10
+ import { ensureCognitoToken } from "../utils/cognito-session.js";
11
+ import { listActiveMembershipCompanies } from "../lib/mesh/api.js";
12
+ import { requireAnywhereRuntime } from "./anywhere-runtime-flag.js";
13
+ import { readCompaniesManifestMap } from "../lib/work-context/repo-remote.js";
14
+ import { canonicalDir, keyForDir, readRegistry, registryDir, registryFile, removeEntriesForDir, resolveCompanyForDir, seedFromManifest, setEntry, writeRegistry, } from "../lib/registry/registry.js";
15
+ async function defaultListMemberships() {
16
+ const token = await ensureCognitoToken({ interactive: false });
17
+ return listActiveMembershipCompanies(token);
18
+ }
19
+ function targetDir(dir) {
20
+ const resolved = path.resolve(dir?.trim() || process.cwd());
21
+ if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) {
22
+ throw Object.assign(new Error(`Not a directory: ${resolved}`), { expected: true });
23
+ }
24
+ return canonicalDir(resolved);
25
+ }
26
+ /** Link `dir` to `company` after confirming the caller is an active member. */
27
+ export async function linkFolder(company, dir, deps = {}) {
28
+ const slug = company.trim();
29
+ if (!slug)
30
+ throw Object.assign(new Error("Company slug is required."), { expected: true });
31
+ const folder = targetDir(dir);
32
+ const memberships = await (deps.listMemberships ?? defaultListMemberships)();
33
+ const match = memberships.find((m) => m.companySlug.toLowerCase() === slug.toLowerCase() || m.companyUid === slug);
34
+ if (!match) {
35
+ throw Object.assign(new Error(`You are not an active member of '${slug}'. Check \`hq whoami\` and the company slug.`), { expected: true });
36
+ }
37
+ const file = deps.file ?? registryFile();
38
+ const registry = readRegistry(file);
39
+ const key = keyForDir(folder);
40
+ setEntry(registry, key, match.companySlug, "link", deps.now?.() ?? new Date());
41
+ writeRegistry(registry, file);
42
+ return { key, company: match.companySlug };
43
+ }
44
+ export function unlinkFolder(dir, deps = {}) {
45
+ const file = deps.file ?? registryFile();
46
+ const registry = readRegistry(file);
47
+ const removed = removeEntriesForDir(registry, targetDir(dir));
48
+ if (removed.length > 0)
49
+ writeRegistry(registry, file);
50
+ return removed;
51
+ }
52
+ export function resolveCompany(dir, deps = {}) {
53
+ const folder = path.resolve(dir?.trim() || process.cwd());
54
+ let registry;
55
+ try {
56
+ registry = readRegistry(deps.file ?? registryFile());
57
+ }
58
+ catch (err) {
59
+ return { company: null, reason: err.message };
60
+ }
61
+ return resolveCompanyForDir(registry, folder);
62
+ }
63
+ /** Seed manifest-sourced entries; called by `hq install --global` (US-005). */
64
+ export function seedRegistryFromHqRoot(hqRoot, deps = {}) {
65
+ const companies = readCompaniesManifestMap(hqRoot);
66
+ if (!companies) {
67
+ throw Object.assign(new Error(`No companies/manifest.yaml under ${hqRoot}`), {
68
+ expected: true,
69
+ });
70
+ }
71
+ const file = deps.file ?? registryFile();
72
+ const registry = readRegistry(file);
73
+ const result = seedFromManifest(registry, path.resolve(hqRoot), companies, deps.now?.() ?? new Date());
74
+ writeRegistry(registry, file);
75
+ return result;
76
+ }
77
+ function printTable(registry) {
78
+ const rows = Object.entries(registry.entries).sort(([a], [b]) => a.localeCompare(b));
79
+ if (rows.length === 0) {
80
+ console.log(chalk.dim(`No linked folders. (${registryFile(registryDir())})`));
81
+ return;
82
+ }
83
+ const width = Math.max(...rows.map(([k]) => k.length), 3);
84
+ console.log(`${"KEY".padEnd(width)} ${"COMPANY".padEnd(20)} ${"SOURCE".padEnd(8)} LINKED AT`);
85
+ for (const [key, e] of rows) {
86
+ console.log(`${key.padEnd(width)} ${e.company.padEnd(20)} ${e.source.padEnd(8)} ${e.linkedAt}`);
87
+ }
88
+ }
89
+ function fail(err) {
90
+ console.error(chalk.red(err instanceof Error ? err.message : String(err)));
91
+ process.exitCode = 1;
92
+ }
93
+ export function registerLinkCommand(program) {
94
+ program
95
+ .command("link [company]")
96
+ .description("Link this folder (or its git remote) to an HQ company")
97
+ .option("--path <dir>", "Folder to link (default: current directory)")
98
+ .option("--list", "Print every linked folder")
99
+ .option("--json", "Print JSON output")
100
+ .addOption(new Option("--seed-manifest <hqRoot>", "Seed entries from an HQ companies/manifest.yaml").hideHelp())
101
+ .action(async (company, opts) => {
102
+ try {
103
+ requireAnywhereRuntime();
104
+ if (opts.list) {
105
+ const registry = readRegistry();
106
+ if (opts.json)
107
+ console.log(JSON.stringify(registry.entries, null, 2));
108
+ else
109
+ printTable(registry);
110
+ return;
111
+ }
112
+ if (opts.seedManifest) {
113
+ const result = seedRegistryFromHqRoot(opts.seedManifest);
114
+ if (opts.json)
115
+ console.log(JSON.stringify(result, null, 2));
116
+ else
117
+ console.log(`Seeded ${result.added.length} manifest entr${result.added.length === 1 ? "y" : "ies"}.`);
118
+ return;
119
+ }
120
+ if (!company) {
121
+ fail(new Error("Usage: hq link <company> [--path <dir>] | hq link --list"));
122
+ return;
123
+ }
124
+ const linked = await linkFolder(company, opts.path);
125
+ if (opts.json)
126
+ console.log(JSON.stringify(linked));
127
+ else
128
+ console.log(`Linked ${linked.key} → ${chalk.bold(linked.company)}`);
129
+ }
130
+ catch (err) {
131
+ fail(err);
132
+ }
133
+ });
134
+ }
135
+ export function registerUnlinkCommand(program) {
136
+ program
137
+ .command("unlink")
138
+ .description("Remove the HQ company link for this folder")
139
+ .option("--path <dir>", "Folder to unlink (default: current directory)")
140
+ .action((opts) => {
141
+ try {
142
+ requireAnywhereRuntime();
143
+ const removed = unlinkFolder(opts.path);
144
+ if (removed.length === 0)
145
+ console.log(chalk.dim("Nothing linked here."));
146
+ else
147
+ for (const key of removed)
148
+ console.log(`Unlinked ${key}`);
149
+ }
150
+ catch (err) {
151
+ fail(err);
152
+ }
153
+ });
154
+ }
155
+ export function registerResolveCompanyCommand(program) {
156
+ program
157
+ .command("resolve-company")
158
+ .description("Print the HQ company linked to this folder (never guesses)")
159
+ .option("--path <dir>", "Folder to resolve (default: current directory)")
160
+ .option("--json", "Print JSON output")
161
+ .action((opts) => {
162
+ try {
163
+ requireAnywhereRuntime();
164
+ }
165
+ catch (err) {
166
+ fail(err);
167
+ return;
168
+ }
169
+ const result = resolveCompany(opts.path);
170
+ if (opts.json) {
171
+ console.log(JSON.stringify(result));
172
+ if (result.company === null)
173
+ process.exitCode = 1;
174
+ return;
175
+ }
176
+ if (result.company !== null)
177
+ console.log(`${result.company}\t${result.key}\t${result.source}`);
178
+ else {
179
+ console.error(result.reason);
180
+ process.exitCode = 1;
181
+ }
182
+ });
183
+ }
184
+ //# sourceMappingURL=link.js.map
@@ -22,6 +22,12 @@ export interface RuntimeServerStatus {
22
22
  headerKeys?: string[];
23
23
  /** Env KEYS present on this runtime's def (values are NEVER captured). */
24
24
  envKeys?: string[];
25
+ /**
26
+ * Whether the def can actually launch: always true for http/sse; for stdio the
27
+ * command is an absolute path that exists and every absolute-path arg exists
28
+ * (policy hq-mcp-absolute-paths). Undefined when not present.
29
+ */
30
+ healthy?: boolean;
25
31
  }
26
32
  /** A single HQ-owned MCP server's cross-runtime status. */
27
33
  export interface McpServerStatus {
@@ -22,6 +22,8 @@
22
22
  * commander; {@link registerMcpCommand} wires the `hq mcp status` command group.
23
23
  */
24
24
  import * as fs from 'fs';
25
+ import * as path from 'path';
26
+ import { anywhereRuntimeEnabled } from './anywhere-runtime-flag.js';
25
27
  import { HQ_PACK_PROVENANCE_KEY, SECRET_REDACTION, backupRoot, claudeConfigFormat, claudeConfigPath, codexConfigFormat, codexConfigPath, CODEX_MCP_SERVERS_KEY, ConfigParseError, isCodexInstalled, readConfigDoc, resolveEnv, } from './mcp-registration.js';
26
28
  // ---------------------------------------------------------------------------
27
29
  // Pure status computation.
@@ -140,7 +142,7 @@ function scanCodex(env) {
140
142
  return { installed: true, parses, configPath, hqServers };
141
143
  }
142
144
  /** Build the per-runtime, per-server status from a scan (or its absence). */
143
- function runtimeServerStatus(scan, name) {
145
+ function runtimeServerStatus(scan, name, checkHealth) {
144
146
  if (!scan.installed)
145
147
  return { presence: 'not-installed' };
146
148
  if (!scan.parses)
@@ -154,8 +156,20 @@ function runtimeServerStatus(scan, name) {
154
156
  target: targetOf(found.def),
155
157
  headerKeys: headerKeysOf(found.def),
156
158
  envKeys: envKeysOf(found.def),
159
+ ...(checkHealth ? { healthy: isLaunchable(found.def) } : {}),
157
160
  };
158
161
  }
162
+ /** True when a def can launch (see {@link RuntimeServerStatus.healthy}). */
163
+ function isLaunchable(def) {
164
+ if (transportOf(def) !== 'stdio')
165
+ return true;
166
+ const command = def.command;
167
+ if (typeof command !== 'string' || !path.isAbsolute(command) || !fs.existsSync(command)) {
168
+ return false;
169
+ }
170
+ const args = Array.isArray(def.args) ? def.args : [];
171
+ return args.every((a) => typeof a !== 'string' || !path.isAbsolute(a) || fs.existsSync(a));
172
+ }
159
173
  /** List the most-recent backup dir names under `backupRoot` (newest first, capped). */
160
174
  function recentBackups(env) {
161
175
  const root = backupRoot(env);
@@ -184,14 +198,15 @@ function recentBackups(env) {
184
198
  * rather than throwing. Header VALUES are never read into the report.
185
199
  */
186
200
  export function computeMcpStatus(env) {
201
+ const checkHealth = anywhereRuntimeEnabled();
187
202
  const claudeScan = scanClaude(env);
188
203
  const codexScan = scanCodex(env);
189
204
  // Union of every HQ-owned server name across both runtimes.
190
205
  const names = new Set([...claudeScan.hqServers.keys(), ...codexScan.hqServers.keys()]);
191
206
  const servers = [];
192
207
  for (const name of [...names].sort()) {
193
- const claude = runtimeServerStatus(claudeScan, name);
194
- const codex = runtimeServerStatus(codexScan, name);
208
+ const claude = runtimeServerStatus(claudeScan, name, checkHealth);
209
+ const codex = runtimeServerStatus(codexScan, name, checkHealth);
195
210
  // Prefer whichever runtime has the def for the top-level transport/target.
196
211
  const claudeFound = claudeScan.hqServers.get(name);
197
212
  const codexFound = codexScan.hqServers.get(name);
@@ -258,7 +273,9 @@ export function computeMcpStatus(env) {
258
273
  function renderRuntimeLine(label, st) {
259
274
  switch (st.presence) {
260
275
  case 'present': {
261
- const parts = [`present`];
276
+ const parts = st.healthy === undefined
277
+ ? ['present']
278
+ : [st.healthy ? 'present (healthy)' : 'present (UNHEALTHY: command not launchable)'];
262
279
  if (st.headerKeys && st.headerKeys.length > 0) {
263
280
  parts.push(`headers: ${st.headerKeys.map((k) => `${k}=${SECRET_REDACTION}`).join(', ')}`);
264
281
  }
@@ -36,14 +36,33 @@ function assertApiKeyCanAuthorizeInstall(source) {
36
36
  }
37
37
  export function registerPackageInstallCommand(parent) {
38
38
  parent
39
- .command('install <source>')
39
+ .command('install [source]')
40
40
  .description('Install a package. Sources: bare slug (registry, Cognito-gated), ' +
41
41
  '@scope/name[@ver] (npm pack), git URL[#ref], or local path.')
42
42
  .option('--company <co>', 'Scope package secret authorization to a specific company')
43
43
  .option('--allow-hooks', 'Skip the hooks confirmation prompt (content-pack flow)')
44
44
  .option('--allow-mcp', 'Skip the MCP server confirmation prompt (content-pack flow)')
45
45
  .option('--branch', 'Follow a ref instead of SHA-pinning (git content-pack flow)')
46
+ .option('--global', 'Install HQ into an agent runtime at user scope (requires the hq-anywhere-runtime flag)')
47
+ .option('--runtime <runtime>', 'Runtime for --global (claude | codex)', 'claude')
48
+ .option('--dry-run', 'With --global: print every file and config diff without writing')
49
+ .option('--hq-root <dir>', 'With --global: HQ checkout to install from (default: walk up from cwd)')
46
50
  .action(async (source, opts) => {
51
+ if (opts.global) {
52
+ if (source) {
53
+ console.error("error: package source cannot be used with --global");
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+ const { runInstallGlobal } = await import('./install-global.js');
58
+ runInstallGlobal(opts);
59
+ return;
60
+ }
61
+ if (!source) {
62
+ console.error("error: missing required argument 'source'");
63
+ process.exitCode = 1;
64
+ return;
65
+ }
47
66
  try {
48
67
  // Gate the top-level `hq install` entrypoint only — `installPack`
49
68
  // itself is also called by `hq packs update` (packs.ts), which must
@@ -3,5 +3,24 @@
3
3
  * Updates lock for specific module
4
4
  */
5
5
  import { Command } from 'commander';
6
+ import { type ConvergenceDeps } from "../lib/daemon/convergence.js";
7
+ export interface RestartAndVerifyDeps extends ConvergenceDeps {
8
+ restart?: () => {
9
+ attempted: boolean;
10
+ restarted: boolean;
11
+ message?: string;
12
+ };
13
+ sendRestartRequest?: () => void;
14
+ home?: string;
15
+ env?: NodeJS.ProcessEnv;
16
+ installedVersion?: () => string | null;
17
+ log?: (line: string) => void;
18
+ }
19
+ /**
20
+ * Restart the daemon after an update and verify it converged on the installed
21
+ * hq-cli (policy hq-self-updater-must-verify-convergence-after-install).
22
+ * Returns false when it did not; the caller must report that as a failure.
23
+ */
24
+ export declare function restartAndVerifyDaemon(hqRoot: string, deps?: RestartAndVerifyDeps): Promise<boolean>;
6
25
  export declare function registerUpdateCommand(program: Command): void;
7
26
  //# sourceMappingURL=update.d.ts.map
@@ -3,9 +3,56 @@
3
3
  * Updates lock for specific module
4
4
  */
5
5
  import * as path from 'path';
6
+ import * as os from "node:os";
6
7
  import { findHqRoot, readManifest, readLock, writeLock, getModulesDir } from '../utils/manifest.js';
7
8
  import { fetchRepo, pullRepo, getCurrentCommit, isRepo } from '../utils/git.js';
8
- import { restartDaemonServiceIfInstalled } from "../lib/mesh/live/daemon/install.js";
9
+ import { daemonPaths } from "../lib/daemon/paths.js";
10
+ import { writeControlRequest } from "../lib/daemon/requests.js";
11
+ import { hqdSocketPath } from "../lib/daemon/protocol.js";
12
+ import { describeNonConvergence, installedCliVersion, pingHqdVersion, verifyDaemonConvergence, } from "../lib/daemon/convergence.js";
13
+ import { anywhereRuntimeEnabled } from "./anywhere-runtime-flag.js";
14
+ /**
15
+ * Restart the daemon after an update and verify it converged on the installed
16
+ * hq-cli (policy hq-self-updater-must-verify-convergence-after-install).
17
+ * Returns false when it did not; the caller must report that as a failure.
18
+ */
19
+ export async function restartAndVerifyDaemon(hqRoot, deps = {}) {
20
+ if (!anywhereRuntimeEnabled())
21
+ return true;
22
+ const log = deps.log ?? ((line) => console.log(line));
23
+ const env = deps.env ?? process.env;
24
+ const home = deps.home ?? os.homedir();
25
+ let restart;
26
+ if (deps.restart)
27
+ restart = deps.restart();
28
+ else {
29
+ try {
30
+ await (deps.ping ?? (() => pingHqdVersion(hqdSocketPath(home, env))))();
31
+ (deps.sendRestartRequest ?? (() => writeControlRequest(daemonPaths(home, env).requestsDir, { action: "restart-daemon" })))();
32
+ restart = { attempted: true, restarted: true };
33
+ }
34
+ catch (err) {
35
+ restart = { attempted: false, restarted: false, message: `hqd is not running; no restart request was sent (${err instanceof Error ? err.message : String(err)})` };
36
+ }
37
+ }
38
+ if (restart.message)
39
+ log(restart.message);
40
+ // No installed daemon: nothing is running old code.
41
+ if (!restart.attempted)
42
+ return true;
43
+ const expected = (deps.installedVersion ?? installedCliVersion)();
44
+ if (!expected) {
45
+ log("Daemon convergence: could not read the installed hq-cli version (hq --version failed). Run: hq doctor --only anywhere");
46
+ return false;
47
+ }
48
+ const result = await verifyDaemonConvergence(expected, deps);
49
+ if (!result.converged) {
50
+ log(describeNonConvergence(result));
51
+ return false;
52
+ }
53
+ log(`Daemon converged: hqd runs hq-cli ${expected}.`);
54
+ return true;
55
+ }
9
56
  export function registerUpdateCommand(program) {
10
57
  program
11
58
  .command('update [module-name]')
@@ -50,12 +97,11 @@ export function registerUpdateCommand(program) {
50
97
  }
51
98
  }
52
99
  writeLock(hqRoot, lock);
53
- // An installed daemon is a separate Node process. Refresh it
54
- // best-effort after a successful update without making the update fail.
55
- const restart = restartDaemonServiceIfInstalled({ hqRoot });
56
- if (restart.message)
57
- console.log(restart.message);
58
100
  console.log('\nLock file updated.');
101
+ // An installed daemon is a separate Node process. Restart it and
102
+ // verify it runs the installed hq-cli; a mismatch fails the command.
103
+ if (!await restartAndVerifyDaemon(hqRoot))
104
+ process.exitCode = 1;
59
105
  }
60
106
  catch (error) {
61
107
  console.error('Error:', error instanceof Error ? error.message : error);
@@ -6,11 +6,12 @@
6
6
  * entries use the bundled-script runner; native entries import their one handler
7
7
  * module. The registration in `core.ts` remains the complete fallback path.
8
8
  */
9
+ import { parseFastCore } from "./fast-scaffold.js";
9
10
  export declare function readStdinIfPiped(stream: NodeJS.ReadStream): Promise<string>;
10
11
  /**
11
12
  * Run the fast-path core command named in `argv` and exit with its code.
12
13
  * Only call when {@link isFastCoreRequest} is true; a non-match returns without
13
14
  * doing anything.
14
15
  */
15
- export declare function runFastCore(argv: readonly string[]): Promise<void>;
16
+ export declare function runFastCore(argv: readonly string[], parsed?: ReturnType<typeof parseFastCore>): Promise<void>;
16
17
  //# sourceMappingURL=fast-core.d.ts.map
package/dist/fast-core.js CHANGED
@@ -6,10 +6,7 @@
6
6
  * entries use the bundled-script runner; native entries import their one handler
7
7
  * module. The registration in `core.ts` remains the complete fallback path.
8
8
  */
9
- import { resolveLiveRoot } from "./utils/hq-roots.js";
10
- import { runBundledScript } from "./utils/run-bundled-script.js";
11
9
  import { parseFastCore } from "./fast-scaffold.js";
12
- import { CORE_RRR_WORKTREE_SCOPE_FIX_FLAG } from "./commands/core-rrr-worktree-scope-fix-flag.js";
13
10
  export async function readStdinIfPiped(stream) {
14
11
  if (stream.isTTY)
15
12
  return "";
@@ -23,8 +20,7 @@ export async function readStdinIfPiped(stream) {
23
20
  * Only call when {@link isFastCoreRequest} is true; a non-match returns without
24
21
  * doing anything.
25
22
  */
26
- export async function runFastCore(argv) {
27
- const parsed = parseFastCore(argv);
23
+ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
28
24
  if (!parsed)
29
25
  return;
30
26
  const entry = parsed.entry;
@@ -41,9 +37,14 @@ export async function runFastCore(argv) {
41
37
  return;
42
38
  }
43
39
  const useShell = entry.kind === "native" && Boolean(entry.familyFlagKey) && !familyEnabled;
44
- const worktreeScopeFixEnabled = entry.name === "repo-run-registry"
45
- ? await (await import("./native-fast-route.js")).fastFlagEnabled(CORE_RRR_WORKTREE_SCOPE_FIX_FLAG, process.env, { numericEnvironmentOverride: false })
46
- : undefined;
40
+ let worktreeScopeFixEnabled;
41
+ if (entry.name === "repo-run-registry") {
42
+ const [{ CORE_RRR_WORKTREE_SCOPE_FIX_FLAG }, { fastFlagEnabled }] = await Promise.all([
43
+ import("./commands/core-rrr-worktree-scope-fix-flag.js"),
44
+ import("./native-fast-route.js"),
45
+ ]);
46
+ worktreeScopeFixEnabled = await fastFlagEnabled(CORE_RRR_WORKTREE_SCOPE_FIX_FLAG, process.env, { numericEnvironmentOverride: false });
47
+ }
47
48
  const shouldInjectRoot = useShell
48
49
  ? entry.shellHqRoot === "live" || (entry.shellHqRoot === "reconcile-default" &&
49
50
  !parsed.args.includes("--hq-root") && !parsed.args.includes("--help") && !parsed.args.includes("-h"))
@@ -56,6 +57,7 @@ export async function runFastCore(argv) {
56
57
  // its expected-user-error formatting remains authoritative.
57
58
  let hqRoot;
58
59
  if (shouldInjectRoot) {
60
+ const { resolveLiveRoot } = await import("./utils/hq-roots.js");
59
61
  try {
60
62
  hqRoot = resolveLiveRoot(parsed.hqRootArg ? { hqRoot: parsed.hqRootArg } : {});
61
63
  }
@@ -109,6 +111,8 @@ export async function runFastCore(argv) {
109
111
  : {}),
110
112
  ...(entry.name === "repo-run-registry" ? { HQ_FLAG_CORE_RRR_WORKTREE_SCOPE_FIX: String(worktreeScopeFixEnabled) } : {}),
111
113
  };
114
+ // Keep the rollback runner off native commands' import path.
115
+ const { runBundledScript } = await import("./utils/run-bundled-script.js");
112
116
  const { code } = runBundledScript({
113
117
  asset,
114
118
  args: parsed.args,
@@ -238,6 +238,19 @@ export const FAST_SCAFFOLD_SCRIPTS = [
238
238
  flagKey: "hq-core-native-fast-route",
239
239
  familyFlagKey: "core-native-hook-helpers",
240
240
  })),
241
+ {
242
+ name: "derive-trigger-facts",
243
+ kind: "native",
244
+ modulePath: "./commands/core-derive-trigger-facts-native.js",
245
+ exportName: "runDeriveTriggerFactsFast",
246
+ flagKey: "hq-core-native-fast-route",
247
+ familyFlagKey: "core-native-hook-helpers",
248
+ asset: "core/scripts/derive-trigger-facts.sh",
249
+ root: "live",
250
+ shellCwd: "live",
251
+ shellHqRoot: "live",
252
+ readsStdin: true,
253
+ },
241
254
  ...["resolve", "pickup", "repo", "secrets", "transfer"].map((name) => ({
242
255
  name: `delegate ${name}`,
243
256
  kind: "native",
package/dist/index.js CHANGED
@@ -18,7 +18,9 @@ import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.
18
18
  // so runCli's finally still finalizes release health and flushes Sentry.
19
19
  installProcessRejectionBoundary();
20
20
  function dispatchCliEntry() {
21
- if (isVersionRequest(process.argv)) {
21
+ const versionRequest = isVersionRequest(process.argv);
22
+ const parsedFastCore = versionRequest ? null : parseFastCore(process.argv);
23
+ if (versionRequest) {
22
24
  // Bare `--version` is the CLI semver only (the contract field updaters
23
25
  // compare). `hq version` and `--json` print the labelled HQ/CLI report.
24
26
  // Lazy so this path still skips the command graph.
@@ -36,22 +38,22 @@ function dispatchCliEntry() {
36
38
  });
37
39
  }
38
40
  }
39
- else if (parseFastCore(process.argv)?.entry.kind === "native" &&
41
+ else if (parsedFastCore?.entry.kind === "native" &&
40
42
  (process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "true" ||
41
43
  process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "1")) {
42
44
  // An explicit enabled override has already decided the global gate. Skip the
43
45
  // snapshot reader import on hot native plumbing paths; runFastCore still
44
46
  // checks the command-family flag before choosing native or shell rollback.
45
- void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
47
+ void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv, parsedFastCore));
46
48
  }
47
- else if (parseFastCore(process.argv)?.entry.kind === "native") {
49
+ else if (parsedFastCore?.entry.kind === "native") {
48
50
  // Native routes are default-off. Resolve the company-scoped persisted flag
49
51
  // without loading the command graph; a miss or stale snapshot takes the
50
52
  // existing full-graph route.
51
53
  void import("./native-fast-route.js").then(async ({ nativeFastRouteEnabled }) => {
52
54
  if (await nativeFastRouteEnabled(process.argv)) {
53
55
  const { runFastCore } = await import("./fast-core.js");
54
- await runFastCore(process.argv);
56
+ await runFastCore(process.argv, parsedFastCore);
55
57
  }
56
58
  else {
57
59
  const { runCli } = await import("./main.js");
@@ -75,11 +77,11 @@ function dispatchCliEntry() {
75
77
  });
76
78
  });
77
79
  }
78
- else if (isFastCoreRequest(process.argv)) {
80
+ else if (parsedFastCore) {
79
81
  // Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
80
82
  // dispatches straight to the bundled-script runner, so it never pays for the
81
83
  // ~60-module command graph main.ts imports. See src/fast-scaffold.ts.
82
- void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv));
84
+ void import("./fast-core.js").then(({ runFastCore }) => runFastCore(process.argv, parsedFastCore));
83
85
  }
84
86
  else {
85
87
  // Keep the command lifecycle detached from module evaluation, as it was
@@ -0,0 +1,25 @@
1
+ export declare const CLI_INVITE_NUDGE_FLAG = "cli.first-sync-invite-teammate-v1";
2
+ export type CliInviteNudgeOutcome = "shown" | "clicked" | "dismissed";
3
+ export interface CliInviteNudgeInput {
4
+ companyUid: string;
5
+ companyRef: string;
6
+ interactive: boolean;
7
+ canPrompt: boolean;
8
+ alreadyShown: boolean;
9
+ flagEnabled: () => Promise<boolean>;
10
+ readHumanMemberCount: () => Promise<number>;
11
+ claimShown: () => boolean;
12
+ ask: (prompt: string) => Promise<boolean>;
13
+ write: (line: string) => void;
14
+ record: (companyUid: string, outcome: CliInviteNudgeOutcome) => void;
15
+ reportFailure?: (operation: string, error: unknown) => void;
16
+ }
17
+ /** One eligible, successful work-bearing company sync can show one prompt per company. */
18
+ export declare function runCliInviteNudge(input: CliInviteNudgeInput): Promise<boolean>;
19
+ /** Called after a successful work-bearing company sync; every dependency remains fail-closed. */
20
+ export declare function maybeShowCliInviteNudge(input: {
21
+ companyUid?: string;
22
+ companyRef?: string;
23
+ accessToken: string;
24
+ }): Promise<boolean>;
25
+ //# sourceMappingURL=cli-invite-nudge.d.ts.map