@indigoai-us/hq-cli 5.341.2 → 5.341.4

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 (35) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/assets/hqd/windows-pipe-host.ps1 +282 -0
  3. package/dist/commands/billing-sponsor.js +20 -1
  4. package/dist/commands/core-derive-trigger-facts-native.d.ts +2 -0
  5. package/dist/commands/core-derive-trigger-facts-native.js +92 -25
  6. package/dist/commands/core-migrate-policy-triggers-native.d.ts +1 -0
  7. package/dist/commands/core-migrate-policy-triggers-native.js +26 -12
  8. package/dist/commands/doctor.js +2 -2
  9. package/dist/lib/aws-sdk-node-warning-output.js +2 -3
  10. package/dist/lib/billing/plan-lock.js +19 -2
  11. package/dist/lib/bot/owner-context.d.ts +2 -0
  12. package/dist/lib/bot/owner-context.js +19 -1
  13. package/dist/lib/daemon/flag-monitor.d.ts +5 -0
  14. package/dist/lib/daemon/flag-monitor.js +14 -1
  15. package/dist/lib/daemon/protocol.d.ts +26 -3
  16. package/dist/lib/daemon/protocol.js +9 -5
  17. package/dist/lib/daemon/remote-control.d.ts +7 -0
  18. package/dist/lib/daemon/remote-control.js +135 -2
  19. package/dist/lib/daemon/run.js +14 -16
  20. package/dist/lib/daemon/server.d.ts +13 -0
  21. package/dist/lib/daemon/server.js +283 -28
  22. package/dist/lib/daemon/windows-pipe-host.d.ts +17 -0
  23. package/dist/lib/daemon/windows-pipe-host.js +218 -0
  24. package/dist/lib/diagnostics.d.ts +9 -0
  25. package/dist/lib/diagnostics.js +12 -0
  26. package/dist/lib/doctor/checks/anywhere.d.ts +6 -1
  27. package/dist/lib/doctor/checks/anywhere.js +131 -13
  28. package/dist/lib/doctor/json-output.d.ts +2 -0
  29. package/dist/lib/doctor/json-output.js +1 -0
  30. package/dist/lib/doctor/registry.js +1 -3
  31. package/dist/lib/doctor/types.d.ts +4 -0
  32. package/dist/utils/run-bundled-script.d.ts +2 -0
  33. package/dist/utils/run-bundled-script.js +10 -1
  34. package/docs/anywhere-runtime.md +32 -5
  35. package/package.json +2 -1
@@ -18,11 +18,12 @@ import { HQ_MCP_SERVER_NAME, SKILL_LINK_PREFIX, claudeTargets } from "../../inst
18
18
  import { codexTargets } from "../../install/codex-targets.js";
19
19
  import { CHATGPT_MODE } from "../../install/chatgpt-targets.js";
20
20
  import { GROK_MODE } from "../../install/grok-targets.js";
21
- import { pingHqdVersion } from "../../daemon/convergence.js";
21
+ import { HqdClientError, hqdRequest } from "../../daemon/protocol.js";
22
22
  import { daemonPresenceLabel, heartbeatFile, readHeartbeat } from "../../daemon/heartbeat.js";
23
23
  import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK } from "../../daemon/install.js";
24
24
  import { hqdSocketPath } from "../../daemon/protocol.js";
25
25
  import { readRegistry, registryDir, registryFile } from "../../registry/registry.js";
26
+ import { anywhereRuntimeEnabled } from "../../../commands/anywhere-runtime-flag.js";
26
27
  export const ANYWHERE_FAMILY_ID = "anywhere";
27
28
  export const REMEDIATION = {
28
29
  installClaude: "hq install --global --runtime claude",
@@ -38,9 +39,69 @@ export const anywhereFamily = createAnywhereFamily();
38
39
  function result(status, checkId, message, remediation, target) {
39
40
  return { status, checkId: `anywhere.${checkId}`, message, ...(remediation ? { remediation } : {}), ...(target ? { target } : {}) };
40
41
  }
41
- export async function runAnywhereChecks(_context, deps = {}) {
42
+ export async function runAnywhereChecks(context, deps = {}) {
42
43
  const home = deps.home ?? os.homedir();
43
44
  const env = deps.env ?? process.env;
45
+ const socketPath = hqdSocketPath(home, env);
46
+ const cwd = context.cwd ?? process.cwd();
47
+ const runtimeEnabled = (deps.runtimeEnabled ?? anywhereRuntimeEnabled)();
48
+ let health;
49
+ let healthUnsupported = false;
50
+ try {
51
+ if (deps.health) {
52
+ health = await deps.health(cwd);
53
+ }
54
+ else if (deps.ping) {
55
+ const version = await deps.ping();
56
+ health = {
57
+ reachable: true,
58
+ version,
59
+ uptimeMs: null,
60
+ flag: { enabled: runtimeEnabled, lastSuccessfulReadAgeMs: null, heldThroughReadError: false },
61
+ resolution: { scope: "personal", company: null, fallbackReason: "no_match" },
62
+ socket: { path: socketPath, transport: process.platform === "win32" ? "windows_named_pipe" : "unix_socket", mode: null, ownerMatchesUser: null, userOnly: null },
63
+ };
64
+ }
65
+ else {
66
+ health = await hqdRequest("health", { cwd }, { socketPath });
67
+ }
68
+ }
69
+ catch (error) {
70
+ if (error instanceof HqdClientError && error.code === "unknown_op") {
71
+ try {
72
+ let version = null;
73
+ let uptimeMs = null;
74
+ if (deps.ping) {
75
+ version = await deps.ping();
76
+ }
77
+ else {
78
+ const ping = await hqdRequest("ping", {}, { socketPath });
79
+ if (typeof ping.version === "string")
80
+ version = ping.version;
81
+ if (typeof ping.uptimeMs === "number")
82
+ uptimeMs = ping.uptimeMs;
83
+ }
84
+ health = {
85
+ reachable: true,
86
+ version,
87
+ uptimeMs,
88
+ flag: { enabled: null, lastSuccessfulReadAgeMs: null, heldThroughReadError: false },
89
+ resolution: { scope: "unknown", company: null, fallbackReason: null },
90
+ socket: { path: socketPath, transport: process.platform === "win32" ? "windows_named_pipe" : "unix_socket", mode: null, ownerMatchesUser: null, userOnly: null },
91
+ };
92
+ healthUnsupported = true;
93
+ }
94
+ catch {
95
+ health = unreachableHealth(socketPath);
96
+ }
97
+ }
98
+ else {
99
+ health = unreachableHealth(socketPath);
100
+ }
101
+ }
102
+ const healthCheck = runtimeHealthCheck(health, healthUnsupported);
103
+ if (!runtimeEnabled)
104
+ return [healthCheck];
44
105
  const claude = claudeTargets(home);
45
106
  const codex = codexTargets(home);
46
107
  const installed = [claude.hqPointer, claude.stateFile, codex.stateFile].some((p) => fs.existsSync(p));
@@ -48,10 +109,10 @@ export async function runAnywhereChecks(_context, deps = {}) {
48
109
  result("WARN", "chatgpt-mode", `ChatGPT integration mode: ${CHATGPT_MODE}; host connector setup is managed in ChatGPT`, "hq install --global --runtime chatgpt"),
49
110
  result("WARN", "grok-mode", `Grok integration mode: ${GROK_MODE}; host connector setup is managed in Grok`, "hq install --global --runtime grok"),
50
111
  ];
112
+ const out = [healthCheck, ...remoteModes];
51
113
  if (!installed) {
52
- return [...remoteModes, result("NA", "installed", "HQ local anywhere runtime is not installed on this machine", REMEDIATION.installClaude)];
114
+ return [...out, result("NA", "installed", "HQ local anywhere runtime is not installed on this machine", REMEDIATION.installClaude)];
53
115
  }
54
- const out = [...remoteModes];
55
116
  const pluginInstall = readClaudePluginInstall(claude.stateFile);
56
117
  out.push(checkInstallMode(claude.stateFile));
57
118
  out.push(checkPointer(claude.hqPointer));
@@ -63,16 +124,11 @@ export async function runAnywhereChecks(_context, deps = {}) {
63
124
  out.push(result("FAIL", "daemon-installed", `daemon service is installed but not loaded (${service.detail})`, REMEDIATION.daemonRestart));
64
125
  else
65
126
  out.push(result("PASS", "daemon-installed", `daemon service is installed and loaded (${service.detail})`));
66
- const socketPath = hqdSocketPath(home, env);
67
- const ping = deps.ping ?? (() => pingHqdVersion(socketPath, 2_000));
68
- let running = null;
69
- try {
70
- running = await ping();
127
+ const running = health.reachable ? health.version : null;
128
+ if (health.reachable)
71
129
  out.push(result("PASS", "socket", `hqd answered on ${socketPath}`, undefined, socketPath));
72
- }
73
- catch (err) {
74
- out.push(result("FAIL", "socket", `hqd is not reachable: ${err instanceof Error ? err.message : String(err)}`, REMEDIATION.daemonInstall, socketPath));
75
- }
130
+ else
131
+ out.push(result("FAIL", "socket", `hqd is not reachable on ${socketPath}`, REMEDIATION.daemonInstall, socketPath));
76
132
  const cliVersion = deps.cliVersion ?? CLI_VERSION;
77
133
  if (running === null)
78
134
  out.push(result("FAIL", "version", `cannot compare versions: hqd did not answer (hq-cli is ${cliVersion})`, REMEDIATION.daemonInstall));
@@ -96,6 +152,68 @@ export async function runAnywhereChecks(_context, deps = {}) {
96
152
  out.push(checkSkillLinks("codex", codex.skillsDir, REMEDIATION.installCodex));
97
153
  return out;
98
154
  }
155
+ function unreachableHealth(socketPath) {
156
+ return {
157
+ reachable: false,
158
+ version: null,
159
+ uptimeMs: null,
160
+ flag: { enabled: null, lastSuccessfulReadAgeMs: null, heldThroughReadError: false },
161
+ resolution: { scope: "unknown", company: null, fallbackReason: null },
162
+ socket: { path: socketPath, transport: process.platform === "win32" ? "windows_named_pipe" : "unix_socket", mode: null, ownerMatchesUser: null, userOnly: null },
163
+ };
164
+ }
165
+ function runtimeHealthCheck(health, healthUnsupported = false) {
166
+ const { flag, resolution, socket } = health;
167
+ const age = flag.lastSuccessfulReadAgeMs === null
168
+ ? "unknown"
169
+ : `${new Intl.NumberFormat("en-US").format(flag.lastSuccessfulReadAgeMs)} ms`;
170
+ const flagLabel = flag.enabled === null ? "unknown" : flag.enabled ? "on" : "off";
171
+ const binding = resolution.scope === "company"
172
+ ? `company ${resolution.company}`
173
+ : resolution.scope === "personal"
174
+ ? `personal (fallback: ${resolution.fallbackReason ?? "unspecified"})`
175
+ : health.reachable && healthUnsupported
176
+ ? "unknown (resident daemon lacks the health operation)"
177
+ : "unknown (daemon unavailable)";
178
+ const socketSecurity = socket.transport === "windows_named_pipe"
179
+ ? "Windows named pipe (mode and owner are not reported)"
180
+ : `mode ${socket.mode ?? "unknown"}; owner matches current user: ${socket.ownerMatchesUser === null ? "unknown" : socket.ownerMatchesUser ? "yes" : "no"}; user-only: ${socket.userOnly === null ? "unknown" : socket.userOnly ? "yes" : "no"}`;
181
+ const unreachableMessage = socket.transport === "windows_named_pipe"
182
+ ? `hqd is not reachable on ${socket.path}; Windows daemon startup is refused because named-pipe ACLs are unsupported`
183
+ : `hqd is not reachable on ${socket.path}; version, uptime, flag state, and cwd binding are unavailable`;
184
+ const socketNotUserOnly = health.reachable && socket.transport === "unix_socket" && (socket.userOnly !== true || socket.ownerMatchesUser !== true);
185
+ const warningNotes = [
186
+ healthUnsupported ? "resident hqd does not support detailed health; restart hqd for full diagnostics" : null,
187
+ socketNotUserOnly ? "socket is not confirmed user-only" : null,
188
+ ].filter((note) => note !== null);
189
+ const message = health.reachable
190
+ ? `hqd reachable; version ${health.version ?? "unknown"}; uptime ${health.uptimeMs === null ? "unknown" : `${new Intl.NumberFormat("en-US").format(health.uptimeMs)} ms`}; runtime flag ${flagLabel}; last successful flag read ${age} ago; held through read error: ${flag.heldThroughReadError ? "yes" : "no"}; cwd binding ${binding}; socket ${socket.path}; ${socketSecurity}${warningNotes.length ? `; ${warningNotes.join("; ")}` : ""}`
191
+ : unreachableMessage;
192
+ const remediation = healthUnsupported || socketNotUserOnly ? REMEDIATION.daemonRestart : undefined;
193
+ return {
194
+ status: health.reachable && warningNotes.length === 0 ? "PASS" : "WARN",
195
+ checkId: "anywhere.runtime-health",
196
+ target: socket.path,
197
+ message,
198
+ ...(remediation ? { remediation } : {}),
199
+ details: {
200
+ reachable: health.reachable,
201
+ version: health.version,
202
+ uptimeMs: health.uptimeMs,
203
+ flagEnabled: flag.enabled,
204
+ lastSuccessfulFlagReadAgeMs: flag.lastSuccessfulReadAgeMs,
205
+ flagHeldThroughReadError: flag.heldThroughReadError,
206
+ resolvedScope: resolution.scope,
207
+ company: resolution.company,
208
+ fallbackReason: resolution.fallbackReason,
209
+ socketPath: socket.path,
210
+ socketTransport: socket.transport,
211
+ socketMode: socket.mode,
212
+ socketOwnerMatchesUser: socket.ownerMatchesUser,
213
+ socketUserOnly: socket.userOnly,
214
+ },
215
+ };
216
+ }
99
217
  function checkInstallMode(file) {
100
218
  try {
101
219
  const state = JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -58,6 +58,8 @@ export interface DoctorJsonResult {
58
58
  target?: string;
59
59
  /** Human-readable explanation. */
60
60
  message: string;
61
+ /** Safe structured diagnostic fields, when a check provides them. */
62
+ details?: Record<string, unknown>;
61
63
  /** Remediation hint. Present only when the result carries one. */
62
64
  remediation?: string;
63
65
  /** Structured remediation (US-011). Present on actionable findings only. */
@@ -60,6 +60,7 @@ function toJsonResult(familyId, result, clientHealthReasonCodesEnabledCheckIds)
60
60
  : {}),
61
61
  ...(result.target !== undefined ? { target: result.target } : {}),
62
62
  message: result.message,
63
+ ...(result.details !== undefined ? { details: result.details } : {}),
63
64
  ...(result.remediation !== undefined
64
65
  ? { remediation: result.remediation }
65
66
  : {}),
@@ -27,7 +27,6 @@ import { anywhereFamily } from "./checks/anywhere.js";
27
27
  import { scaffoldCliFloorFamily } from "./checks/scaffold-cli-floor.js";
28
28
  import { fixtureCoverageFamily } from "./fixtures/discover.js";
29
29
  import { checkClaudeWiring } from "./checks/claude-wiring.js";
30
- import { anywhereRuntimeEnabled } from "../../commands/anywhere-runtime-flag.js";
31
30
  /**
32
31
  * An ordered, id-keyed set of check families. Registration order is preserved
33
32
  * (Map iteration order), and re-registering an id replaces the family in place
@@ -266,8 +265,7 @@ export function createDefaultRegistry() {
266
265
  registry.register(botsFamily);
267
266
  // HQ anywhere (US-012): pointer, registry, hqd service/socket/version,
268
267
  // heartbeat, MCP registrations and skill links. NA when not installed.
269
- if (anywhereRuntimeEnabled())
270
- registry.register(anywhereFamily);
268
+ registry.register(anywhereFamily);
271
269
  return registry;
272
270
  }
273
271
  //# sourceMappingURL=registry.js.map
@@ -35,6 +35,8 @@ export interface CheckResult {
35
35
  target?: string;
36
36
  /** Human-readable one-line explanation of the result. */
37
37
  message: string;
38
+ /** Safe structured fields for machine-readable results. */
39
+ details?: Record<string, unknown>;
38
40
  /** Optional remediation hint (a command or instruction). */
39
41
  remediation?: string;
40
42
  }
@@ -47,6 +49,8 @@ export interface CheckResult {
47
49
  export interface CheckContext {
48
50
  /** Absolute, resolved HQ tree root every check reads from. */
49
51
  hqRoot: string;
52
+ /** Original caller working directory, used only for company/personal binding diagnostics. */
53
+ cwd?: string;
50
54
  /**
51
55
  * The detected host platform (id, and optionally the evidence behind it).
52
56
  * Host-specific live checks — the US-006 runtime probe — key their
@@ -51,6 +51,8 @@ export type RunBundledScriptOptions = {
51
51
  hqRoot?: string;
52
52
  /** Extra environment for the child, applied last. */
53
53
  env?: Record<string, string | undefined>;
54
+ /** Convert the script path for Git Bash when the script resolves sibling assets from BASH_SOURCE. */
55
+ msysScriptPath?: boolean;
54
56
  /** Capture stdout and stderr as bytes instead of inheriting those streams. */
55
57
  captureOutput?: boolean;
56
58
  /** Bytes supplied on stdin. With no value, stdin is inherited as before. */
@@ -44,6 +44,14 @@ export function interpreterFor(asset) {
44
44
  const ext = path.extname(asset).toLowerCase();
45
45
  return ext === ".mjs" || ext === ".js" || ext === ".cjs" ? "node" : "bash";
46
46
  }
47
+ /** Git Bash expects drive paths in MSYS form when an interpreter arg is a script path. */
48
+ function scriptPathForBash(script) {
49
+ if (process.platform !== "win32")
50
+ return script;
51
+ const normalized = script.replaceAll("\\", "/");
52
+ const drivePath = normalized.match(/^([A-Za-z]):\/(.*)$/);
53
+ return drivePath ? `/${drivePath[1].toLowerCase()}/${drivePath[2]}` : normalized;
54
+ }
47
55
  function envValue(env, name) {
48
56
  const key = Object.keys(env).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
49
57
  return key ? env[key] : undefined;
@@ -176,7 +184,8 @@ export function runBundledScript(opts) {
176
184
  : inputDescriptor !== undefined
177
185
  ? [stdinStdio, "inherit", "inherit"]
178
186
  : "inherit";
179
- result = spawnSync(executable, [script, ...(opts.args ?? [])], {
187
+ const scriptArg = interpreter === "bash" && opts.msysScriptPath ? scriptPathForBash(script) : script;
188
+ result = spawnSync(executable, [scriptArg, ...(opts.args ?? [])], {
180
189
  cwd: opts.cwd,
181
190
  env,
182
191
  stdio,
@@ -1,8 +1,9 @@
1
1
  # HQ Anywhere runtime
2
2
 
3
3
  HQ Anywhere installs global hooks for supported agent runtimes and uses the
4
- local hqd daemon on macOS and Linux. Windows does not currently start hqd, so
5
- the daemon-backed routing described here is unavailable there. A per-user
4
+ local hqd daemon on macOS, Linux, and Windows. On Windows, hqd uses a bundled
5
+ PowerShell pipe host that applies and verifies a user-only named-pipe DACL
6
+ before becoming ready. A per-user
6
7
  registry links repositories to HQ companies. The `hq-anywhere-runtime` flag
7
8
  controls these features and is off by default.
8
9
 
@@ -26,9 +27,19 @@ The CLI stores the HQ root at `~/.hq/root` and keeps the folder registry in
26
27
  `~/.hq/registry.json`. These files belong to the current host. They are not
27
28
  stored in a repository or synced HQ folder.
28
29
 
29
- On Windows, `hq daemon install` reports that hqd cannot start because its
30
- named-pipe ACL is not supported. Global hook installation may still configure
31
- local runtime hooks, but it does not provide the hqd routing described below.
30
+ Windows requires Windows PowerShell 5.1 or PowerShell 7 for the bundled pipe
31
+ host. If it is unavailable, the pipe cannot be created with a verified
32
+ current-user-only DACL, or another process already owns the pipe name, hqd
33
+ refuses to start. The `hq-anywhere-runtime` flag remains off by default on all
34
+ platforms.
35
+
36
+ On macOS and Linux, hqd checks the socket directory and socket path with
37
+ `lstat` before serving. It refuses symlinked, shared, or foreign-owned paths.
38
+ The default socket is under `~/.hq/daemon/`, separate from the registry directory;
39
+ a directory created for the socket is set to `0700`. hqd replaces a stale
40
+ socket only when the current user owns it, then verifies the listening socket
41
+ is `0600`. If a check fails, hqd reports the path and stops without removing an
42
+ unexpected file.
32
43
 
33
44
  When the flag is enabled, the global hook uses hqd for events outside the HQ
34
45
  root. Inside the root, the project hook routes through hqd when it is running
@@ -51,6 +62,22 @@ An authentication still in progress when revocation arrives fails cleanly;
51
62
  hosted MCP calls already forwarded are allowed to finish or return their normal
52
63
  request error. The daemon process itself does not terminate.
53
64
 
65
+ ## Diagnose the runtime
66
+
67
+ `hq doctor` reports hqd reachability, version and uptime, the last successful
68
+ runtime-flag read and its age, and whether hqd is holding that decision after a
69
+ read error. It also reports the company selected for the current directory or
70
+ the reason it fell back to personal context. On Unix, the socket line includes
71
+ its permission mode, owner check and whether it is user-only. Windows reports
72
+ the named-pipe transport without Unix mode or owner fields. Use `hq doctor
73
+ --json` for the same fields as structured details. The hqd health operation is
74
+ read-only and available while the runtime flag is off; it reports only the
75
+ current directory's binding and never returns authentication tokens. A Unix
76
+ socket that cannot be verified as user-only produces a warning. If an older
77
+ resident daemon does not support the health operation, doctor falls back to
78
+ `ping` for reachability, version and uptime, then warns that a restart is needed
79
+ for full diagnostics.
80
+
54
81
  ## Link a repository
55
82
 
56
83
  Run `hq link <company>` from a repository to associate it with a company. The
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.341.2",
3
+ "version": "5.341.4",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -13,6 +13,7 @@
13
13
  "assets/scaffold",
14
14
  "assets/bot-workers",
15
15
  "assets/mesh-daemon",
16
+ "assets/hqd",
16
17
  "assets/lanes-reconcile",
17
18
  "assets/lanes-hooks",
18
19
  "templates",