@indigoai-us/hq-cli 5.323.0 → 5.325.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 (73) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/dist/bin/hq-auth-refresh.d.ts +5 -1
  3. package/dist/bin/hq-auth-refresh.js +33 -1
  4. package/dist/command-catalog.generated.d.ts +125 -6
  5. package/dist/command-catalog.generated.js +162 -6
  6. package/dist/command-registration-plan.d.ts +24 -0
  7. package/dist/command-registration-plan.js +4 -0
  8. package/dist/commands/anywhere-runtime-flag.d.ts +9 -0
  9. package/dist/commands/anywhere-runtime-flag.js +22 -0
  10. package/dist/commands/core-derive-trigger-facts-native.js +37 -3
  11. package/dist/commands/daemon.js +39 -2
  12. package/dist/commands/db-provision.js +6 -1
  13. package/dist/commands/doctor.js +8 -4
  14. package/dist/commands/feedback.d.ts +2 -0
  15. package/dist/commands/feedback.js +10 -0
  16. package/dist/commands/install-global.d.ts +46 -0
  17. package/dist/commands/install-global.js +674 -0
  18. package/dist/commands/link.d.ts +30 -0
  19. package/dist/commands/link.js +184 -0
  20. package/dist/commands/mcp-status.d.ts +6 -0
  21. package/dist/commands/mcp-status.js +21 -4
  22. package/dist/commands/pkg-install.js +20 -1
  23. package/dist/commands/update.d.ts +19 -0
  24. package/dist/commands/update.js +52 -6
  25. package/dist/fast-core.d.ts +2 -1
  26. package/dist/fast-core.js +12 -8
  27. package/dist/index.js +9 -7
  28. package/dist/lib/core-utils/timeout-guard-native-flags.d.ts +6 -0
  29. package/dist/lib/core-utils/timeout-guard-native-flags.js +30 -0
  30. package/dist/lib/core-utils/timeout-guard-native.d.ts +0 -3
  31. package/dist/lib/core-utils/timeout-guard-native.js +4 -30
  32. package/dist/lib/core-utils/timeout-guard.d.ts +2 -0
  33. package/dist/lib/core-utils/timeout-guard.js +2 -1
  34. package/dist/lib/daemon/convergence.d.ts +42 -0
  35. package/dist/lib/daemon/convergence.js +66 -0
  36. package/dist/lib/daemon/heartbeat.d.ts +43 -0
  37. package/dist/lib/daemon/heartbeat.js +70 -0
  38. package/dist/lib/daemon/ops/journal.d.ts +29 -0
  39. package/dist/lib/daemon/ops/journal.js +141 -0
  40. package/dist/lib/daemon/ops/memory.d.ts +20 -0
  41. package/dist/lib/daemon/ops/memory.js +65 -0
  42. package/dist/lib/daemon/ops/mesh.d.ts +38 -0
  43. package/dist/lib/daemon/ops/mesh.js +70 -0
  44. package/dist/lib/daemon/ops/policy.d.ts +78 -0
  45. package/dist/lib/daemon/ops/policy.js +283 -0
  46. package/dist/lib/daemon/protocol.d.ts +68 -0
  47. package/dist/lib/daemon/protocol.js +154 -0
  48. package/dist/lib/daemon/run.d.ts +10 -0
  49. package/dist/lib/daemon/run.js +85 -3
  50. package/dist/lib/daemon/server.d.ts +36 -0
  51. package/dist/lib/daemon/server.js +306 -0
  52. package/dist/lib/daemon/session-registry.d.ts +30 -0
  53. package/dist/lib/daemon/session-registry.js +42 -0
  54. package/dist/lib/doctor/checks/anywhere.d.ts +38 -0
  55. package/dist/lib/doctor/checks/anywhere.js +182 -0
  56. package/dist/lib/doctor/registry.js +6 -0
  57. package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
  58. package/dist/lib/flag-registry-worker-source.generated.js +1 -1
  59. package/dist/lib/install/claude-targets.d.ts +47 -0
  60. package/dist/lib/install/claude-targets.js +203 -0
  61. package/dist/lib/install/codex-targets.d.ts +57 -0
  62. package/dist/lib/install/codex-targets.js +187 -0
  63. package/dist/lib/mesh/live/session-seq.js +62 -9
  64. package/dist/lib/registry/normalize-remote.d.ts +21 -0
  65. package/dist/lib/registry/normalize-remote.js +68 -0
  66. package/dist/lib/registry/registry.d.ts +75 -0
  67. package/dist/lib/registry/registry.js +261 -0
  68. package/dist/utils/cognito-session.d.ts +5 -1
  69. package/dist/utils/cognito-session.js +8 -12
  70. package/dist/utils/feedback-diagnostics.d.ts +2 -0
  71. package/dist/utils/version-check.d.ts +2 -0
  72. package/dist/utils/version-check.js +7 -0
  73. package/package.json +2 -2
@@ -0,0 +1,36 @@
1
+ /**
2
+ * hqd socket server (hq-anywhere US-008).
3
+ *
4
+ * Hosted inside the `hq daemon run` supervisor, which already runs the mesh
5
+ * drainer as one of its services, so there is still one resident process per
6
+ * machine. Sessions in any repo ask it which company they belong to and
7
+ * register themselves here.
8
+ */
9
+ import { type ResolveResult } from "../registry/registry.js";
10
+ import { SessionRegistry } from "./session-registry.js";
11
+ export interface HqdServerOptions {
12
+ socketPath: string;
13
+ /** registry.json to resolve against (US-004). */
14
+ registryPath?: string;
15
+ hqRoot?: string;
16
+ version?: string;
17
+ now?: () => Date;
18
+ /** Called after a `shutdown` request has been answered. */
19
+ onShutdown?: () => void;
20
+ log?: (level: "info" | "warn" | "error", message: string) => void;
21
+ /** Environment for policy scripts and the mesh spool (tests sandbox HOME here). */
22
+ env?: NodeJS.ProcessEnv;
23
+ /** Work-mesh root override for mesh.presence (tests). */
24
+ workMeshRoot?: string;
25
+ /** Crash reporter for internal op failures (US-012); run.ts wires Sentry here. */
26
+ reportError?: (op: string, err: unknown) => void;
27
+ }
28
+ export interface HqdServer {
29
+ socketPath: string;
30
+ sessions: SessionRegistry;
31
+ close(): Promise<void>;
32
+ }
33
+ /** Resolve cwd → company through the registry, cached until registry.json changes. */
34
+ export declare function createResolver(registryPath: string): (cwd: string) => ResolveResult;
35
+ export declare function startHqdServer(opts: HqdServerOptions): Promise<HqdServer>;
36
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1,306 @@
1
+ /**
2
+ * hqd socket server (hq-anywhere US-008).
3
+ *
4
+ * Hosted inside the `hq daemon run` supervisor, which already runs the mesh
5
+ * drainer as one of its services, so there is still one resident process per
6
+ * machine. Sessions in any repo ask it which company they belong to and
7
+ * register themselves here.
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as net from "node:net";
11
+ import * as path from "node:path";
12
+ import { CLI_VERSION } from "../../cli-version.js";
13
+ import { canonicalDir, readRegistry, registryFile, resolveCompanyForDir } from "../registry/registry.js";
14
+ import { createLineSplitter, encodeMessage, parseRequestLine } from "./protocol.js";
15
+ import { SessionRegistry } from "./session-registry.js";
16
+ import { journalAppend, JournalOpError } from "./ops/journal.js";
17
+ import { memoryGet, memoryPut, MemoryOpError } from "./ops/memory.js";
18
+ import { meshPresence, MeshOpError } from "./ops/mesh.js";
19
+ import { PolicyIndex, PolicyOpError, policyCheck } from "./ops/policy.js";
20
+ class OpError extends Error {
21
+ code;
22
+ constructor(code, message) {
23
+ super(message);
24
+ this.code = code;
25
+ }
26
+ }
27
+ function stringArg(args, name, required = true) {
28
+ const value = args[name];
29
+ if (typeof value === "string" && value.trim())
30
+ return value;
31
+ if (value === undefined && !required)
32
+ return undefined;
33
+ throw new OpError("bad_args", `${name} must be a non-empty string`);
34
+ }
35
+ /** Resolve cwd → company through the registry, cached until registry.json changes. */
36
+ export function createResolver(registryPath) {
37
+ const cache = new Map();
38
+ let stamp = "";
39
+ return (cwd) => {
40
+ let current = "missing";
41
+ try {
42
+ const st = fs.statSync(registryPath);
43
+ current = `${st.mtimeMs}:${st.size}:${st.ino}`;
44
+ }
45
+ catch (err) {
46
+ if (err.code !== "ENOENT")
47
+ throw err;
48
+ }
49
+ if (current !== stamp) {
50
+ cache.clear();
51
+ stamp = current;
52
+ }
53
+ const key = canonicalDir(cwd);
54
+ const hit = cache.get(key);
55
+ if (hit)
56
+ return hit;
57
+ const result = resolveCompanyForDir(readRegistry(registryPath), key);
58
+ cache.set(key, result);
59
+ return result;
60
+ };
61
+ }
62
+ /** Is something answering on this socket? */
63
+ function socketIsLive(socketPath) {
64
+ return new Promise((resolve) => {
65
+ const probe = net.createConnection(socketPath);
66
+ const done = (live) => {
67
+ probe.destroy();
68
+ resolve(live);
69
+ };
70
+ probe.once("connect", () => done(true));
71
+ probe.once("error", () => done(false));
72
+ setTimeout(() => done(false), 500).unref();
73
+ });
74
+ }
75
+ export async function startHqdServer(opts) {
76
+ const { socketPath } = opts;
77
+ const isPipe = process.platform === "win32";
78
+ const startedAtMs = (opts.now ?? (() => new Date()))().getTime();
79
+ const sessions = new SessionRegistry(opts.now);
80
+ const resolve = createResolver(opts.registryPath ?? registryFile());
81
+ const log = opts.log ?? (() => { });
82
+ const clock = opts.now ?? (() => new Date());
83
+ const env = opts.env ?? process.env;
84
+ const policyIndex = opts.hqRoot ? new PolicyIndex(opts.hqRoot, log) : null;
85
+ const requireRoot = () => {
86
+ if (!opts.hqRoot)
87
+ throw new OpError("no_hq_root", "hqd has no HQ root configured (set hqRoot in daemon config or HQ_ROOT)");
88
+ return opts.hqRoot;
89
+ };
90
+ /** Company for a call: explicit arg, else the open session's, else the registry's answer for cwd. */
91
+ const companyFor = (args) => {
92
+ const explicit = stringArg(args, "company", false);
93
+ if (explicit)
94
+ return explicit;
95
+ const runtime = stringArg(args, "runtime", false);
96
+ const sessionId = stringArg(args, "sessionId", false);
97
+ const session = runtime && sessionId ? sessions.get(runtime, sessionId) : null;
98
+ if (session?.company)
99
+ return session.company;
100
+ const cwd = stringArg(args, "cwd", false);
101
+ return cwd ? (resolve(cwd).company ?? undefined) : undefined;
102
+ };
103
+ if (!isPipe) {
104
+ fs.mkdirSync(path.dirname(socketPath), { recursive: true, mode: 0o700 });
105
+ if (fs.existsSync(socketPath)) {
106
+ if (await socketIsLive(socketPath))
107
+ throw new Error(`hqd is already listening on ${socketPath}`);
108
+ fs.unlinkSync(socketPath);
109
+ }
110
+ }
111
+ /** Synchronous ops answer in request order; only policy.check returns a promise. */
112
+ const handle = (req) => {
113
+ const args = req.args ?? {};
114
+ switch (req.op) {
115
+ case "ping":
116
+ return {
117
+ version: opts.version ?? CLI_VERSION,
118
+ hqRoot: opts.hqRoot ?? null,
119
+ uptimeMs: clock().getTime() - startedAtMs,
120
+ pid: process.pid,
121
+ sessions: sessions.size,
122
+ };
123
+ case "resolve": {
124
+ const cwd = stringArg(args, "cwd");
125
+ return resolve(cwd);
126
+ }
127
+ case "session.open": {
128
+ const runtime = stringArg(args, "runtime");
129
+ const sessionId = stringArg(args, "sessionId");
130
+ const cwd = stringArg(args, "cwd");
131
+ const explicit = stringArg(args, "company", false);
132
+ const company = explicit ?? resolve(cwd).company;
133
+ return sessions.open({ runtime, sessionId, cwd: canonicalDir(cwd), company });
134
+ }
135
+ case "session.get": {
136
+ const runtime = stringArg(args, "runtime", false);
137
+ const sessionId = stringArg(args, "sessionId", false);
138
+ if (!sessionId) {
139
+ const all = sessions.list();
140
+ return { sessions: runtime ? all.filter((s) => s.runtime === runtime) : all };
141
+ }
142
+ if (!runtime)
143
+ throw new OpError("bad_args", "runtime is required with sessionId");
144
+ return { session: sessions.get(runtime, sessionId) };
145
+ }
146
+ case "session.close": {
147
+ const runtime = stringArg(args, "runtime");
148
+ const sessionId = stringArg(args, "sessionId");
149
+ return { closed: sessions.close(runtime, sessionId) };
150
+ }
151
+ case "policy.check": {
152
+ const hqRoot = requireRoot();
153
+ const company = companyFor(args);
154
+ if (!company)
155
+ throw new OpError("bad_args", "company is required (or a cwd linked to a company)");
156
+ const input = args.input;
157
+ if (input !== undefined && (input === null || typeof input !== "object" || Array.isArray(input))) {
158
+ throw new OpError("bad_args", "input must be an object");
159
+ }
160
+ return policyCheck({
161
+ company,
162
+ event: stringArg(args, "event", false),
163
+ tool: stringArg(args, "tool"),
164
+ input: input,
165
+ sessionId: stringArg(args, "sessionId", false),
166
+ cwd: stringArg(args, "cwd", false),
167
+ }, { hqRoot, index: policyIndex, env, now: clock });
168
+ }
169
+ case "policy.index": {
170
+ requireRoot();
171
+ const company = stringArg(args, "company", false);
172
+ return { version: policyIndex.version, policies: policyIndex.list(company) };
173
+ }
174
+ case "journal.append": {
175
+ const company = companyFor(args);
176
+ if (!company)
177
+ throw new OpError("bad_args", "company is required (or a cwd linked to a company)");
178
+ return journalAppend({ company, project: stringArg(args, "project"), section: stringArg(args, "section"), text: stringArg(args, "text") }, requireRoot(), clock());
179
+ }
180
+ case "memory.get":
181
+ return memoryGet(requireRoot(), stringArg(args, "sessionId"), stringArg(args, "key", false));
182
+ case "memory.put":
183
+ return memoryPut(requireRoot(), stringArg(args, "sessionId"), stringArg(args, "key"), args.value, clock());
184
+ case "mesh.presence": {
185
+ const harness = stringArg(args, "harness", false) ?? stringArg(args, "runtime");
186
+ return meshPresence({
187
+ sessionId: stringArg(args, "sessionId"),
188
+ harness,
189
+ kind: stringArg(args, "kind"),
190
+ cwd: stringArg(args, "cwd", false),
191
+ company: companyFor(args),
192
+ project: stringArg(args, "project", false),
193
+ task: stringArg(args, "task", false),
194
+ taskId: stringArg(args, "taskId", false),
195
+ status: stringArg(args, "status", false),
196
+ summary: stringArg(args, "summary", false),
197
+ reason: stringArg(args, "reason", false),
198
+ }, { hqRoot: opts.hqRoot, workMeshRoot: opts.workMeshRoot, env, now: clock });
199
+ }
200
+ case "shutdown":
201
+ return { stopping: true };
202
+ default:
203
+ throw new OpError("unknown_op", `unknown op ${req.op}`);
204
+ }
205
+ };
206
+ const respond = (socket, msg) => {
207
+ if (!socket.destroyed)
208
+ socket.write(encodeMessage(msg));
209
+ };
210
+ const clients = new Set();
211
+ const server = net.createServer((socket) => {
212
+ clients.add(socket);
213
+ socket.on("close", () => clients.delete(socket));
214
+ socket.on("error", (err) => log("warn", `hqd client error: ${err.message}`));
215
+ const push = createLineSplitter((line) => {
216
+ const parsed = parseRequestLine(line);
217
+ if (!parsed.ok) {
218
+ respond(socket, { id: parsed.id, ok: false, error: parsed.error });
219
+ return;
220
+ }
221
+ const req = parsed.request;
222
+ const succeed = (result) => {
223
+ respond(socket, { id: req.id, ok: true, result });
224
+ if (req.op === "shutdown") {
225
+ log("info", "hqd received a shutdown request");
226
+ setImmediate(() => opts.onShutdown?.());
227
+ }
228
+ };
229
+ const fail = (err) => {
230
+ const error = err instanceof OpError ||
231
+ err instanceof PolicyOpError ||
232
+ err instanceof JournalOpError ||
233
+ err instanceof MemoryOpError ||
234
+ err instanceof MeshOpError
235
+ ? { code: err.code, message: err.message }
236
+ : { code: "internal", message: err instanceof Error ? err.message : String(err) };
237
+ if (error.code === "internal") {
238
+ log("error", `hqd ${req.op} failed: ${error.message}`);
239
+ try {
240
+ opts.reportError?.(req.op, err);
241
+ }
242
+ catch (reportErr) {
243
+ log("warn", `hqd could not report the ${req.op} failure: ${reportErr.message}`);
244
+ }
245
+ }
246
+ respond(socket, { id: req.id, ok: false, error });
247
+ };
248
+ let result;
249
+ try {
250
+ result = handle(req);
251
+ }
252
+ catch (err) {
253
+ fail(err);
254
+ return;
255
+ }
256
+ if (result instanceof Promise)
257
+ result.then(succeed, fail);
258
+ else
259
+ succeed(result);
260
+ }, () => {
261
+ respond(socket, { id: null, ok: false, error: { code: "too_large", message: "request line too large" } });
262
+ socket.end();
263
+ });
264
+ socket.on("data", push);
265
+ });
266
+ await new Promise((resolveListen, rejectListen) => {
267
+ server.once("error", rejectListen);
268
+ // Bind with a user-only umask so the socket is never briefly group/world accessible.
269
+ const prior = isPipe ? null : process.umask(0o177);
270
+ try {
271
+ server.listen(socketPath, () => {
272
+ server.off("error", rejectListen);
273
+ resolveListen();
274
+ });
275
+ }
276
+ finally {
277
+ if (prior !== null)
278
+ process.umask(prior);
279
+ }
280
+ });
281
+ if (!isPipe)
282
+ fs.chmodSync(socketPath, 0o600);
283
+ log("info", `hqd listening on ${socketPath}`);
284
+ let closing = null;
285
+ return {
286
+ socketPath,
287
+ sessions,
288
+ close: () => (closing ??= new Promise((done) => {
289
+ policyIndex?.close();
290
+ server.close(() => done());
291
+ // Answered-but-idle clients would otherwise hold close() open.
292
+ for (const client of clients)
293
+ client.destroy();
294
+ if (!isPipe) {
295
+ try {
296
+ fs.unlinkSync(socketPath);
297
+ }
298
+ catch (err) {
299
+ if (err.code !== "ENOENT")
300
+ log("warn", `could not remove ${socketPath}: ${err.message}`);
301
+ }
302
+ }
303
+ })),
304
+ };
305
+ }
306
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * In-memory session registry hosted by hqd (hq-anywhere US-008). Keyed by
3
+ * runtime + session id; nothing is persisted, so a daemon restart starts empty
4
+ * and sessions re-open on their next hook.
5
+ */
6
+ export interface HqdSession {
7
+ runtime: string;
8
+ sessionId: string;
9
+ cwd: string;
10
+ company: string | null;
11
+ startedAt: string;
12
+ }
13
+ export declare function sessionKey(runtime: string, sessionId: string): string;
14
+ export declare class SessionRegistry {
15
+ private readonly now;
16
+ private readonly sessions;
17
+ constructor(now?: () => Date);
18
+ /** Open or refresh a session. Re-opening keeps the original startedAt. */
19
+ open(input: {
20
+ runtime: string;
21
+ sessionId: string;
22
+ cwd: string;
23
+ company: string | null;
24
+ }): HqdSession;
25
+ get(runtime: string, sessionId: string): HqdSession | null;
26
+ close(runtime: string, sessionId: string): boolean;
27
+ list(): HqdSession[];
28
+ get size(): number;
29
+ }
30
+ //# sourceMappingURL=session-registry.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * In-memory session registry hosted by hqd (hq-anywhere US-008). Keyed by
3
+ * runtime + session id; nothing is persisted, so a daemon restart starts empty
4
+ * and sessions re-open on their next hook.
5
+ */
6
+ export function sessionKey(runtime, sessionId) {
7
+ return `${runtime}\u0000${sessionId}`;
8
+ }
9
+ export class SessionRegistry {
10
+ now;
11
+ sessions = new Map();
12
+ constructor(now = () => new Date()) {
13
+ this.now = now;
14
+ }
15
+ /** Open or refresh a session. Re-opening keeps the original startedAt. */
16
+ open(input) {
17
+ const key = sessionKey(input.runtime, input.sessionId);
18
+ const prior = this.sessions.get(key);
19
+ const session = {
20
+ runtime: input.runtime,
21
+ sessionId: input.sessionId,
22
+ cwd: input.cwd,
23
+ company: input.company,
24
+ startedAt: prior?.startedAt ?? this.now().toISOString(),
25
+ };
26
+ this.sessions.set(key, session);
27
+ return session;
28
+ }
29
+ get(runtime, sessionId) {
30
+ return this.sessions.get(sessionKey(runtime, sessionId)) ?? null;
31
+ }
32
+ close(runtime, sessionId) {
33
+ return this.sessions.delete(sessionKey(runtime, sessionId));
34
+ }
35
+ list() {
36
+ return [...this.sessions.values()];
37
+ }
38
+ get size() {
39
+ return this.sessions.size;
40
+ }
41
+ }
42
+ //# sourceMappingURL=session-registry.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `hq doctor` anywhere family (hq-anywhere US-012).
3
+ *
4
+ * Checks the pieces that let a session in any repo pick up HQ: the ~/.hq/root
5
+ * pointer, the repo registry, the hqd daemon service and socket, version
6
+ * convergence between hqd and the installed hq-cli, the heartbeat, the Claude
7
+ * and Codex MCP registrations, and the hq-* skill links. Every line is PASS,
8
+ * WARN or FAIL with a remediation command. When anywhere was never installed
9
+ * on this machine the family reports a single NA line.
10
+ */
11
+ import type { CheckContext, CheckFamily, CheckResult } from "../types.js";
12
+ export declare const ANYWHERE_FAMILY_ID = "anywhere";
13
+ export declare const REMEDIATION: {
14
+ readonly installClaude: "hq install --global --runtime claude";
15
+ readonly installCodex: "hq install --global --runtime codex";
16
+ readonly daemonInstall: "hq daemon install";
17
+ readonly daemonRestart: "hq daemon restart --daemon";
18
+ readonly link: "hq link <repo-dir> <company>";
19
+ };
20
+ export interface DaemonServiceState {
21
+ installed: boolean;
22
+ loaded: boolean;
23
+ detail: string;
24
+ }
25
+ export interface AnywhereDeps {
26
+ home?: string;
27
+ env?: NodeJS.ProcessEnv;
28
+ /** Version hqd reports; rejects when the socket is unreachable. */
29
+ ping?: () => Promise<string>;
30
+ /** The hq-cli version installed on this machine. Default: this CLI's version. */
31
+ cliVersion?: string;
32
+ serviceState?: () => DaemonServiceState;
33
+ now?: () => Date;
34
+ }
35
+ export declare function createAnywhereFamily(deps?: AnywhereDeps): CheckFamily;
36
+ export declare const anywhereFamily: CheckFamily;
37
+ export declare function runAnywhereChecks(_context: CheckContext, deps?: AnywhereDeps): Promise<CheckResult[]>;
38
+ //# sourceMappingURL=anywhere.d.ts.map
@@ -0,0 +1,182 @@
1
+ /**
2
+ * `hq doctor` anywhere family (hq-anywhere US-012).
3
+ *
4
+ * Checks the pieces that let a session in any repo pick up HQ: the ~/.hq/root
5
+ * pointer, the repo registry, the hqd daemon service and socket, version
6
+ * convergence between hqd and the installed hq-cli, the heartbeat, the Claude
7
+ * and Codex MCP registrations, and the hq-* skill links. Every line is PASS,
8
+ * WARN or FAIL with a remediation command. When anywhere was never installed
9
+ * on this machine the family reports a single NA line.
10
+ */
11
+ import { spawnSync } from "node:child_process";
12
+ import * as fs from "node:fs";
13
+ import * as os from "node:os";
14
+ import * as path from "node:path";
15
+ import { CLI_VERSION } from "../../../cli-version.js";
16
+ import { computeMcpStatus } from "../../../commands/mcp-status.js";
17
+ import { HQ_MCP_SERVER_NAME, SKILL_LINK_PREFIX, claudeTargets } from "../../install/claude-targets.js";
18
+ import { codexTargets } from "../../install/codex-targets.js";
19
+ import { pingHqdVersion } from "../../daemon/convergence.js";
20
+ import { daemonPresenceLabel, heartbeatFile, readHeartbeat } from "../../daemon/heartbeat.js";
21
+ import { DAEMON_LAUNCHD_LABEL, DAEMON_SYSTEMD_UNIT, DAEMON_WINDOWS_TASK } from "../../daemon/install.js";
22
+ import { hqdSocketPath } from "../../daemon/protocol.js";
23
+ import { readRegistry, registryDir, registryFile } from "../../registry/registry.js";
24
+ export const ANYWHERE_FAMILY_ID = "anywhere";
25
+ export const REMEDIATION = {
26
+ installClaude: "hq install --global --runtime claude",
27
+ installCodex: "hq install --global --runtime codex",
28
+ daemonInstall: "hq daemon install",
29
+ daemonRestart: "hq daemon restart --daemon",
30
+ link: "hq link <repo-dir> <company>",
31
+ };
32
+ export function createAnywhereFamily(deps = {}) {
33
+ return { id: ANYWHERE_FAMILY_ID, title: "HQ anywhere runtime", run: (context) => runAnywhereChecks(context, deps) };
34
+ }
35
+ export const anywhereFamily = createAnywhereFamily();
36
+ function result(status, checkId, message, remediation, target) {
37
+ return { status, checkId: `anywhere.${checkId}`, message, ...(remediation ? { remediation } : {}), ...(target ? { target } : {}) };
38
+ }
39
+ export async function runAnywhereChecks(_context, deps = {}) {
40
+ const home = deps.home ?? os.homedir();
41
+ const env = deps.env ?? process.env;
42
+ const claude = claudeTargets(home);
43
+ const codex = codexTargets(home);
44
+ const installed = [claude.hqPointer, claude.stateFile, codex.stateFile].some((p) => fs.existsSync(p));
45
+ if (!installed) {
46
+ return [result("NA", "installed", "HQ anywhere is not installed on this machine", REMEDIATION.installClaude)];
47
+ }
48
+ const out = [];
49
+ out.push(checkPointer(claude.hqPointer));
50
+ out.push(checkRegistry(registryFile(registryDir(home, env))));
51
+ const service = (deps.serviceState ?? (() => defaultServiceState(home)))();
52
+ if (!service.installed)
53
+ out.push(result("FAIL", "daemon-installed", `daemon service is not installed (${service.detail})`, REMEDIATION.daemonInstall));
54
+ else if (!service.loaded)
55
+ out.push(result("FAIL", "daemon-installed", `daemon service is installed but not loaded (${service.detail})`, REMEDIATION.daemonRestart));
56
+ else
57
+ out.push(result("PASS", "daemon-installed", `daemon service is installed and loaded (${service.detail})`));
58
+ const socketPath = hqdSocketPath(home, env);
59
+ const ping = deps.ping ?? (() => pingHqdVersion(socketPath, 2_000));
60
+ let running = null;
61
+ try {
62
+ running = await ping();
63
+ out.push(result("PASS", "socket", `hqd answered on ${socketPath}`, undefined, socketPath));
64
+ }
65
+ catch (err) {
66
+ out.push(result("FAIL", "socket", `hqd is not reachable: ${err instanceof Error ? err.message : String(err)}`, REMEDIATION.daemonInstall, socketPath));
67
+ }
68
+ const cliVersion = deps.cliVersion ?? CLI_VERSION;
69
+ if (running === null)
70
+ out.push(result("FAIL", "version", `cannot compare versions: hqd did not answer (hq-cli is ${cliVersion})`, REMEDIATION.daemonInstall));
71
+ else if (running !== cliVersion)
72
+ out.push(result("FAIL", "version", `hqd runs ${running} but hq-cli ${cliVersion} is installed`, REMEDIATION.daemonRestart));
73
+ else
74
+ out.push(result("PASS", "version", `hqd and hq-cli both run ${cliVersion}`));
75
+ out.push(checkHeartbeat(heartbeatFile(home, env), (deps.now ?? (() => new Date()))()));
76
+ const mcp = computeMcpStatus({ home });
77
+ const server = mcp.servers.find((s) => s.server === HQ_MCP_SERVER_NAME);
78
+ out.push(checkRegistration("claude", server?.claude, mcp.runtimes.claude, REMEDIATION.installClaude));
79
+ out.push(checkRegistration("codex", server?.codex, mcp.runtimes.codex, REMEDIATION.installCodex));
80
+ out.push(checkSkillLinks("claude", claude.skillsDir, REMEDIATION.installClaude));
81
+ if (mcp.runtimes.codex.installed)
82
+ out.push(checkSkillLinks("codex", codex.skillsDir, REMEDIATION.installCodex));
83
+ return out;
84
+ }
85
+ function checkPointer(file) {
86
+ let root;
87
+ try {
88
+ root = fs.readFileSync(file, "utf8").trim();
89
+ }
90
+ catch {
91
+ return result("FAIL", "pointer", `${file} is missing`, REMEDIATION.installClaude, file);
92
+ }
93
+ const valid = root && path.isAbsolute(root) && [".claude", "core"].every((m) => fs.existsSync(path.join(root, m)));
94
+ if (!valid)
95
+ return result("FAIL", "pointer", `${file} points at ${root || "(empty)"}, which is not an HQ root`, REMEDIATION.installClaude, file);
96
+ return result("PASS", "pointer", `${file} points at ${root}`, undefined, file);
97
+ }
98
+ function checkRegistry(file) {
99
+ if (!fs.existsSync(file))
100
+ return result("WARN", "registry", `${file} does not exist yet; no repos are linked`, REMEDIATION.link, file);
101
+ try {
102
+ const registry = readRegistry(file);
103
+ const count = Object.keys(registry.entries).length;
104
+ return result("PASS", "registry", `${file} is readable (${count} entries)`, undefined, file);
105
+ }
106
+ catch (err) {
107
+ return result("FAIL", "registry", `${file} is unreadable: ${err.message}`, `mv ${file} ${file}.bad && ${REMEDIATION.installClaude}`, file);
108
+ }
109
+ }
110
+ function checkHeartbeat(file, now) {
111
+ let state;
112
+ try {
113
+ state = readHeartbeat(file);
114
+ }
115
+ catch (err) {
116
+ return result("FAIL", "heartbeat", `heartbeat record is unreadable: ${err.message}`, REMEDIATION.daemonRestart, file);
117
+ }
118
+ if (!state)
119
+ return result("WARN", "heartbeat", "no hqd heartbeat recorded yet", REMEDIATION.daemonRestart, file);
120
+ if (daemonPresenceLabel(state, now) !== "daemon live")
121
+ return result("WARN", "heartbeat", `daemon stale: last heartbeat at ${state.at}`, REMEDIATION.daemonRestart, file);
122
+ return result("PASS", "heartbeat", `last heartbeat at ${state.at} (hqd ${state.version}, ${state.sessions} sessions)`, undefined, file);
123
+ }
124
+ function checkRegistration(runtimeName, status, runtimeState, fix) {
125
+ const id = `${runtimeName}-registration`;
126
+ if (!runtimeState.installed)
127
+ return result("WARN", id, `${runtimeName} is not installed, so hq is not registered there`, fix);
128
+ if (!runtimeState.parses)
129
+ return result("FAIL", id, `${runtimeName} MCP config cannot be parsed`, fix);
130
+ if (!status || status.presence === "absent" || status.presence === "not-installed")
131
+ return result("WARN", id, `the hq MCP server is not registered in ${runtimeName}`, fix);
132
+ if (status.presence === "unparseable")
133
+ return result("FAIL", id, `${runtimeName} MCP config cannot be parsed`, fix);
134
+ if (!status.healthy)
135
+ return result("FAIL", id, `${runtimeName} hq MCP entry does not use absolute paths that exist (${status.target ?? ""})`, fix);
136
+ return result("PASS", id, `hq MCP server registered in ${runtimeName} with absolute paths`);
137
+ }
138
+ function checkSkillLinks(runtime, dir, fix) {
139
+ const id = `${runtime}-skills`;
140
+ let names;
141
+ try {
142
+ names = fs.readdirSync(dir).filter((n) => n.startsWith(SKILL_LINK_PREFIX));
143
+ }
144
+ catch {
145
+ return result("WARN", id, `${dir} does not exist`, fix, dir);
146
+ }
147
+ const links = names.filter((n) => fs.lstatSync(path.join(dir, n)).isSymbolicLink());
148
+ if (links.length === 0)
149
+ return result("WARN", id, `no hq-* skill links in ${dir}`, fix, dir);
150
+ const broken = links.filter((n) => !fs.existsSync(path.join(dir, n)));
151
+ if (broken.length > 0)
152
+ return result("FAIL", id, `${broken.length} broken skill links in ${dir}: ${broken.slice(0, 5).join(", ")}`, fix, dir);
153
+ return result("PASS", id, `${links.length} hq-* skill links in ${dir} resolve`, undefined, dir);
154
+ }
155
+ function defaultServiceState(home) {
156
+ if (process.platform === "darwin") {
157
+ const plist = path.join(home, "Library", "LaunchAgents", `${DAEMON_LAUNCHD_LABEL}.plist`);
158
+ if (!fs.existsSync(plist))
159
+ return { installed: false, loaded: false, detail: `no ${plist}` };
160
+ const uid = process.getuid?.() ?? 0;
161
+ const probe = spawnSync("launchctl", ["print", `gui/${uid}/${DAEMON_LAUNCHD_LABEL}`], { encoding: "utf8", timeout: 5_000 });
162
+ return { installed: true, loaded: probe.status === 0, detail: `launchd ${DAEMON_LAUNCHD_LABEL}` };
163
+ }
164
+ if (process.platform === "linux") {
165
+ const unit = path.join(home, ".config", "systemd", "user", DAEMON_SYSTEMD_UNIT);
166
+ const systemUnit = path.join("/etc/systemd/system", DAEMON_SYSTEMD_UNIT);
167
+ if (!fs.existsSync(unit) && !fs.existsSync(systemUnit))
168
+ return { installed: false, loaded: false, detail: `no ${DAEMON_SYSTEMD_UNIT}` };
169
+ const systemScope = fs.existsSync(systemUnit);
170
+ const probe = spawnSync("systemctl", [...(systemScope ? [] : ["--user"]), "is-active", DAEMON_SYSTEMD_UNIT], { encoding: "utf8", timeout: 5_000 });
171
+ return { installed: true, loaded: probe.status === 0, detail: `systemd ${systemScope ? "system" : "user"} ${DAEMON_SYSTEMD_UNIT}` };
172
+ }
173
+ if (process.platform === "win32") {
174
+ const probe = spawnSync("schtasks.exe", ["/Query", "/TN", DAEMON_WINDOWS_TASK, "/FO", "CSV", "/NH"], { encoding: "utf8", timeout: 5_000 });
175
+ if (probe.status !== 0)
176
+ return { installed: false, loaded: false, detail: `no Scheduled Task ${DAEMON_WINDOWS_TASK}` };
177
+ const loaded = /,\s*"?Running"?\s*$/i.test(probe.stdout.trim());
178
+ return { installed: true, loaded, detail: `Scheduled Task ${DAEMON_WINDOWS_TASK}${loaded ? " is running" : " is not running"}` };
179
+ }
180
+ return { installed: false, loaded: false, detail: `${process.platform}: service probe not available` };
181
+ }
182
+ //# sourceMappingURL=anywhere.js.map
@@ -23,9 +23,11 @@ import { integrationsFamily } from "./checks/integrations.js";
23
23
  import { syncHealthFamily } from "./checks/sync-health.js";
24
24
  import { workContextFamily } from "./checks/work-context.js";
25
25
  import { botsFamily } from "./checks/bots.js";
26
+ import { anywhereFamily } from "./checks/anywhere.js";
26
27
  import { scaffoldCliFloorFamily } from "./checks/scaffold-cli-floor.js";
27
28
  import { fixtureCoverageFamily } from "./fixtures/discover.js";
28
29
  import { checkClaudeWiring } from "./checks/claude-wiring.js";
30
+ import { anywhereRuntimeEnabled } from "../../commands/anywhere-runtime-flag.js";
29
31
  /**
30
32
  * An ordered, id-keyed set of check families. Registration order is preserved
31
33
  * (Map iteration order), and re-registering an id replaces the family in place
@@ -262,6 +264,10 @@ export function createDefaultRegistry() {
262
264
  // Local bots: inbox/heartbeat reachability and a LaunchAgent whose hq
263
265
  // path survived an HQ toolchain move. Local-only reads.
264
266
  registry.register(botsFamily);
267
+ // HQ anywhere (US-012): pointer, registry, hqd service/socket/version,
268
+ // heartbeat, MCP registrations and skill links. NA when not installed.
269
+ if (anywhereRuntimeEnabled())
270
+ registry.register(anywhereFamily);
265
271
  return registry;
266
272
  }
267
273
  //# sourceMappingURL=registry.js.map