@indigoai-us/hq-cli 5.54.1 → 5.55.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.
@@ -9,6 +9,18 @@
9
9
  *
10
10
  * Keeps a hidden `master-sync` alias for one release so an updated CLI still
11
11
  * answers a not-yet-updated hook shim (and vice-versa) during rollout.
12
+ *
13
+ * Lock-wait policy: reindex shares one per-root operation lock with `sync` and
14
+ * `rescue`. By default acquisition waits UNBOUNDED for a live holder, which is
15
+ * what a human running `hq reindex` interactively wants. But when invoked from a
16
+ * Claude/Codex lifecycle hook (SessionStart / UserPromptSubmit / Stop /
17
+ * PostToolUse), waiting is exactly wrong: if a sync/rescue is mid-flight, the
18
+ * hook blocks the agent up to the host's per-hook timeout (the multi-minute
19
+ * "Claude won't load in the HQ folder" spinner). `--from-hook` makes the hook
20
+ * path refuse-fast (never wait); `--lock-timeout <sec>` bounds it explicitly.
21
+ * The bound is applied via the HQ_OP_LOCK_TIMEOUT env var that the hq-cloud
22
+ * operation lock honors, so it works even against an installed hq-cloud build
23
+ * that predates a typed lock-timeout option.
12
24
  */
13
25
  import { Command } from 'commander';
14
26
  export declare function registerReindexCommand(program: Command): void;
@@ -1,5 +1,5 @@
1
1
 
2
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="9d81df7d-0153-5f51-9cfb-60f1409c6b94")}catch(e){}}();
2
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="91aee9b3-dd72-5f05-99a8-d23b37f5eabe")}catch(e){}}();
3
3
  import { reindex } from '@indigoai-us/hq-cloud';
4
4
  export function registerReindexCommand(program) {
5
5
  program
@@ -7,10 +7,32 @@ export function registerReindexCommand(program) {
7
7
  .alias('master-sync')
8
8
  .description('Surface namespaced skills, mirror the personal overlay into core/, and regenerate the workers registry')
9
9
  .option('--repo-root <path>', 'HQ root to operate on (defaults to the current directory)')
10
+ .option('--from-hook', 'Invoked from a Claude/Codex lifecycle hook: never wait on the per-root operation lock — if a sync/rescue holds it, skip this reindex instead of blocking the session (equivalent to --lock-timeout 0)')
11
+ .option('--lock-timeout <seconds>', 'Bound the wait for the per-root operation lock (seconds). 0 = refuse immediately; omitted = wait indefinitely (the interactive default). --from-hook implies 0; an explicit --lock-timeout wins.')
10
12
  .action((opts) => {
13
+ // Resolve the lock-wait bound. An explicit --lock-timeout wins; otherwise
14
+ // --from-hook forces 0 (refuse-fast) so a hook fired while a sync/rescue
15
+ // holds the shared per-root operation lock can never stall the agent.
16
+ let lockTimeoutSec;
17
+ if (opts.lockTimeout !== undefined) {
18
+ const parsed = Number(opts.lockTimeout);
19
+ if (Number.isFinite(parsed) && parsed >= 0)
20
+ lockTimeoutSec = parsed;
21
+ }
22
+ else if (opts.fromHook) {
23
+ lockTimeoutSec = 0;
24
+ }
25
+ // Apply via HQ_OP_LOCK_TIMEOUT — the env var the hq-cloud operation lock
26
+ // reads (acquireOperationLock → resolveWaitConfig). Going through the env
27
+ // (rather than a typed reindex() option) keeps this forward/backward
28
+ // compatible with any installed hq-cloud build. Never clobber a value the
29
+ // caller already set explicitly.
30
+ if (lockTimeoutSec !== undefined && process.env.HQ_OP_LOCK_TIMEOUT === undefined) {
31
+ process.env.HQ_OP_LOCK_TIMEOUT = String(lockTimeoutSec);
32
+ }
11
33
  const { status } = reindex({ repoRoot: opts.repoRoot });
12
34
  process.exit(status);
13
35
  });
14
36
  }
15
37
  //# sourceMappingURL=reindex.js.map
16
- //# debugId=9d81df7d-0153-5f51-9cfb-60f1409c6b94
38
+ //# debugId=91aee9b3-dd72-5f05-99a8-d23b37f5eabe
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.54.1",
3
+ "version": "5.55.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,94 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ vi,
8
+ type MockInstance,
9
+ } from "vitest";
10
+
11
+ // Mock the hq-cloud reindex() so the command runs without touching the FS.
12
+ vi.mock("@indigoai-us/hq-cloud", () => ({
13
+ reindex: vi.fn(() => ({ status: 0 })),
14
+ }));
15
+
16
+ import { Command } from "commander";
17
+ import { reindex } from "@indigoai-us/hq-cloud";
18
+ import { registerReindexCommand } from "./reindex.js";
19
+
20
+ const reindexMock = reindex as unknown as MockInstance<typeof reindex>;
21
+
22
+ function buildProgram(): Command {
23
+ const program = new Command();
24
+ program.exitOverride();
25
+ program.configureOutput({
26
+ writeOut: () => undefined,
27
+ writeErr: () => undefined,
28
+ });
29
+ registerReindexCommand(program);
30
+ return program;
31
+ }
32
+
33
+ async function run(...args: string[]): Promise<void> {
34
+ // process.exit is spied to throw; swallow that so parseAsync resolves.
35
+ try {
36
+ await buildProgram().parseAsync(["node", "hq", "reindex", ...args]);
37
+ } catch (err) {
38
+ if (!(err instanceof Error) || !err.message.startsWith("__EXIT__:")) throw err;
39
+ }
40
+ }
41
+
42
+ let exitSpy: MockInstance<typeof process.exit>;
43
+ let savedLockTimeout: string | undefined;
44
+
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+ savedLockTimeout = process.env.HQ_OP_LOCK_TIMEOUT;
48
+ delete process.env.HQ_OP_LOCK_TIMEOUT;
49
+ exitSpy = vi.spyOn(process, "exit").mockImplementation(((code?: number) => {
50
+ throw new Error(`__EXIT__:${code ?? 0}`);
51
+ }) as never);
52
+ });
53
+
54
+ afterEach(() => {
55
+ vi.restoreAllMocks();
56
+ if (savedLockTimeout === undefined) delete process.env.HQ_OP_LOCK_TIMEOUT;
57
+ else process.env.HQ_OP_LOCK_TIMEOUT = savedLockTimeout;
58
+ });
59
+
60
+ describe("hq reindex lock-wait policy", () => {
61
+ it("--from-hook forces a no-wait lock acquisition (HQ_OP_LOCK_TIMEOUT=0)", async () => {
62
+ await run("--from-hook");
63
+ expect(process.env.HQ_OP_LOCK_TIMEOUT).toBe("0");
64
+ expect(reindexMock).toHaveBeenCalledTimes(1);
65
+ expect(exitSpy).toHaveBeenCalledWith(0);
66
+ });
67
+
68
+ it("--lock-timeout <sec> sets an explicit bounded wait", async () => {
69
+ await run("--lock-timeout", "5");
70
+ expect(process.env.HQ_OP_LOCK_TIMEOUT).toBe("5");
71
+ });
72
+
73
+ it("an explicit --lock-timeout wins over --from-hook", async () => {
74
+ await run("--from-hook", "--lock-timeout", "10");
75
+ expect(process.env.HQ_OP_LOCK_TIMEOUT).toBe("10");
76
+ });
77
+
78
+ it("a plain interactive reindex leaves the wait unbounded (env unset)", async () => {
79
+ await run();
80
+ expect(process.env.HQ_OP_LOCK_TIMEOUT).toBeUndefined();
81
+ expect(reindexMock).toHaveBeenCalledTimes(1);
82
+ });
83
+
84
+ it("never clobbers an HQ_OP_LOCK_TIMEOUT the caller already set", async () => {
85
+ process.env.HQ_OP_LOCK_TIMEOUT = "42";
86
+ await run("--from-hook");
87
+ expect(process.env.HQ_OP_LOCK_TIMEOUT).toBe("42");
88
+ });
89
+
90
+ it("ignores an invalid --lock-timeout (negative / non-numeric)", async () => {
91
+ await run("--lock-timeout", "nope");
92
+ expect(process.env.HQ_OP_LOCK_TIMEOUT).toBeUndefined();
93
+ });
94
+ });
@@ -9,6 +9,18 @@
9
9
  *
10
10
  * Keeps a hidden `master-sync` alias for one release so an updated CLI still
11
11
  * answers a not-yet-updated hook shim (and vice-versa) during rollout.
12
+ *
13
+ * Lock-wait policy: reindex shares one per-root operation lock with `sync` and
14
+ * `rescue`. By default acquisition waits UNBOUNDED for a live holder, which is
15
+ * what a human running `hq reindex` interactively wants. But when invoked from a
16
+ * Claude/Codex lifecycle hook (SessionStart / UserPromptSubmit / Stop /
17
+ * PostToolUse), waiting is exactly wrong: if a sync/rescue is mid-flight, the
18
+ * hook blocks the agent up to the host's per-hook timeout (the multi-minute
19
+ * "Claude won't load in the HQ folder" spinner). `--from-hook` makes the hook
20
+ * path refuse-fast (never wait); `--lock-timeout <sec>` bounds it explicitly.
21
+ * The bound is applied via the HQ_OP_LOCK_TIMEOUT env var that the hq-cloud
22
+ * operation lock honors, so it works even against an installed hq-cloud build
23
+ * that predates a typed lock-timeout option.
12
24
  */
13
25
  import { Command } from 'commander';
14
26
  import { reindex } from '@indigoai-us/hq-cloud';
@@ -21,7 +33,35 @@ export function registerReindexCommand(program: Command): void {
21
33
  'Surface namespaced skills, mirror the personal overlay into core/, and regenerate the workers registry'
22
34
  )
23
35
  .option('--repo-root <path>', 'HQ root to operate on (defaults to the current directory)')
24
- .action((opts: { repoRoot?: string }) => {
36
+ .option(
37
+ '--from-hook',
38
+ 'Invoked from a Claude/Codex lifecycle hook: never wait on the per-root operation lock — if a sync/rescue holds it, skip this reindex instead of blocking the session (equivalent to --lock-timeout 0)'
39
+ )
40
+ .option(
41
+ '--lock-timeout <seconds>',
42
+ 'Bound the wait for the per-root operation lock (seconds). 0 = refuse immediately; omitted = wait indefinitely (the interactive default). --from-hook implies 0; an explicit --lock-timeout wins.'
43
+ )
44
+ .action((opts: { repoRoot?: string; fromHook?: boolean; lockTimeout?: string }) => {
45
+ // Resolve the lock-wait bound. An explicit --lock-timeout wins; otherwise
46
+ // --from-hook forces 0 (refuse-fast) so a hook fired while a sync/rescue
47
+ // holds the shared per-root operation lock can never stall the agent.
48
+ let lockTimeoutSec: number | undefined;
49
+ if (opts.lockTimeout !== undefined) {
50
+ const parsed = Number(opts.lockTimeout);
51
+ if (Number.isFinite(parsed) && parsed >= 0) lockTimeoutSec = parsed;
52
+ } else if (opts.fromHook) {
53
+ lockTimeoutSec = 0;
54
+ }
55
+
56
+ // Apply via HQ_OP_LOCK_TIMEOUT — the env var the hq-cloud operation lock
57
+ // reads (acquireOperationLock → resolveWaitConfig). Going through the env
58
+ // (rather than a typed reindex() option) keeps this forward/backward
59
+ // compatible with any installed hq-cloud build. Never clobber a value the
60
+ // caller already set explicitly.
61
+ if (lockTimeoutSec !== undefined && process.env.HQ_OP_LOCK_TIMEOUT === undefined) {
62
+ process.env.HQ_OP_LOCK_TIMEOUT = String(lockTimeoutSec);
63
+ }
64
+
25
65
  const { status } = reindex({ repoRoot: opts.repoRoot });
26
66
  process.exit(status);
27
67
  });