@indigoai-us/hq-cli 5.345.115 → 5.345.117

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.
package/CHANGELOG.md CHANGED
@@ -2,9 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.345.117] — 2026-10-13
6
+ - `hq lanes monitor-check` answers a session that owns no active lane from the lane-record directory alone, without loading the command graph. The Claude Stop hook runs it after every turn in every session; per-call CPU drops from about 545 ms to about 60 ms. Sessions with an active lane, corrupt records, or conflict sidecars still use the full command.
7
+
8
+ ## [5.345.116] — 2026-10-13
9
+
5
10
  ## [5.345.115] — 2026-10-13
6
11
  - The bundled delegate-send shell now caches the Git Bash `jq --binary` capability probe for one invocation instead of starting `jq.exe` before every jq operation.
7
12
  - Remove the bundled scheduled jobs shell copies and route scheduled job units, probes, validation, notifications, and reconciliation through native HQ core commands. Fixed shell-output fixtures retain differential coverage for all 238 cases, and the published E2E matrix now runs native-only probes for these commands.
13
+ - `hq core scan-packages` no longer refuses to wire a pack worker because a worker with the same id sits inside a tenant's `repos/` checkout. Its id-collision check now skips the directories the worker registry skips (tenant `repos/`, `_template`, `_overrides`), so pack workers such as `qa-tester` link again.
8
14
 
9
15
  - `hq core checkpoint` now runs its background sibling under a supervisor that
10
16
  stops it after 30 minutes and kills any process it left behind, and the
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ import { CLI_COMMAND_HELP_CATALOG_FLAG } from "./lib/cli-flag-keys.js";
13
13
  import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.js";
14
14
  import { installFastPathStreamErrorHandlers } from "./utils/fast-stream-errors.js";
15
15
  import { fileURLToPath } from "node:url";
16
+ import { parseMonitorCheckFastRequest } from "./lanes-monitor-check-argv.js";
16
17
  import { createRequire } from "node:module";
17
18
  function cygpathMixedPath(value) {
18
19
  // Require only a Node builtin: the package supports Node releases before
@@ -143,6 +144,16 @@ async function runNativeFastRouteWithRecovery(parsed) {
143
144
  await runMainWithInstallTreeRecovery();
144
145
  }
145
146
  }
147
+ async function runMonitorCheckFastWithRecovery(request) {
148
+ const loaded = await recoverFastImport(() => import("./lanes-monitor-check-fast.js"));
149
+ if (loaded.reexecStatus !== undefined) {
150
+ process.exitCode = loaded.reexecStatus;
151
+ return;
152
+ }
153
+ if (loaded.module?.runMonitorCheckFast(request))
154
+ return;
155
+ await runMainWithInstallTreeRecovery();
156
+ }
146
157
  function dispatchCliEntry() {
147
158
  if (process.platform === "win32" && coreCommandHasPathOperands(process.argv)) {
148
159
  // Keep cygpath off non-Windows and non-path command routes.
@@ -181,6 +192,12 @@ function dispatchCliEntry() {
181
192
  await runMainWithInstallTreeRecovery();
182
193
  });
183
194
  }
195
+ else if (parseMonitorCheckFastRequest(process.argv)) {
196
+ // The Stop hook runs this every turn in every session. Answer the common
197
+ // no-active-lane case from the lane-record directory alone; anything the
198
+ // fast route cannot prove falls back to the full command graph.
199
+ void runMonitorCheckFastWithRecovery(parseMonitorCheckFastRequest(process.argv));
200
+ }
184
201
  else if (parsedFastCore?.entry.kind === "native" &&
185
202
  (process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "true" ||
186
203
  process.env.HQ_FLAG_HQ_CORE_NATIVE_FAST_ROUTE === "1")) {
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Dependency-free argv matcher for the `hq lanes monitor-check` fast route.
3
+ * Imported statically by src/index.ts, so it must not import anything.
4
+ */
5
+ export interface MonitorCheckFastRequest {
6
+ sessionId: string;
7
+ engine?: string;
8
+ json: boolean;
9
+ }
10
+ /** Exactly `hq lanes monitor-check --session <id> [--engine <e>] [--json]`. */
11
+ export declare function parseMonitorCheckFastRequest(argv: readonly string[]): MonitorCheckFastRequest | null;
12
+ //# sourceMappingURL=lanes-monitor-check-argv.d.ts.map
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Dependency-free argv matcher for the `hq lanes monitor-check` fast route.
3
+ * Imported statically by src/index.ts, so it must not import anything.
4
+ */
5
+ const SAFE_SESSION_ID = /^[A-Za-z0-9._:-]{1,200}$/u;
6
+ /** Exactly `hq lanes monitor-check --session <id> [--engine <e>] [--json]`. */
7
+ export function parseMonitorCheckFastRequest(argv) {
8
+ if (argv[2] !== "lanes" || argv[3] !== "monitor-check")
9
+ return null;
10
+ let sessionId;
11
+ let engine;
12
+ let json = false;
13
+ const args = argv.slice(4);
14
+ for (let i = 0; i < args.length; i++) {
15
+ const arg = args[i];
16
+ let name = arg;
17
+ let value;
18
+ const eq = arg.indexOf("=");
19
+ if (arg.startsWith("--") && eq > 0) {
20
+ name = arg.slice(0, eq);
21
+ value = arg.slice(eq + 1);
22
+ }
23
+ if (name === "--json" && value === undefined) {
24
+ if (json)
25
+ return null;
26
+ json = true;
27
+ }
28
+ else if (name === "--session" || name === "--engine") {
29
+ if (value === undefined) {
30
+ value = args[++i];
31
+ if (value === undefined || value.startsWith("-"))
32
+ return null;
33
+ }
34
+ if (name === "--session") {
35
+ if (sessionId !== undefined)
36
+ return null;
37
+ sessionId = value;
38
+ }
39
+ else {
40
+ if (engine !== undefined)
41
+ return null;
42
+ engine = value;
43
+ }
44
+ }
45
+ else {
46
+ return null;
47
+ }
48
+ }
49
+ const trimmed = sessionId?.trim();
50
+ if (!trimmed || !SAFE_SESSION_ID.test(trimmed))
51
+ return null;
52
+ return { sessionId: trimmed, engine, json };
53
+ }
54
+ //# sourceMappingURL=lanes-monitor-check-argv.js.map
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Startup fast route for `hq lanes monitor-check`.
3
+ *
4
+ * The Claude Stop hook (`lanes-senior-monitor-stop-gate.sh`) runs this command
5
+ * at the end of every assistant turn in every HQ session, and the
6
+ * SessionStart/UserPromptSubmit shim runs it too. Through the full command
7
+ * graph each call cost ~0.6-0.9 s of CPU, almost all of it module loading
8
+ * (Commander graph, Sentry, undici, the flag-registry worker). With several
9
+ * sessions open that kept one core busy continuously.
10
+ *
11
+ * Nearly every call is for a session that owns no active lane, so the answer
12
+ * is an empty result that needs only the lane-record directory. This module
13
+ * answers that case with `fs` alone. It returns `false` (caller falls back to
14
+ * the full command) whenever the answer could differ from the full path:
15
+ * unusual argv, an unresolvable root, an unreadable store, a conflict sidecar,
16
+ * a record that does not parse, or any record that may name this session as an
17
+ * active senior. The Monitor evidence scan never runs here.
18
+ */
19
+ import type { MonitorCheckFastRequest } from "./lanes-monitor-check-argv.js";
20
+ export { parseMonitorCheckFastRequest } from "./lanes-monitor-check-argv.js";
21
+ export type { MonitorCheckFastRequest } from "./lanes-monitor-check-argv.js";
22
+ export interface MonitorCheckFastDeps {
23
+ env?: NodeJS.ProcessEnv;
24
+ cwd?: string;
25
+ readdirSync?: (dir: string) => string[];
26
+ readFileSync?: (file: string) => string;
27
+ write?: (text: string) => void;
28
+ }
29
+ export interface MonitorCheckFastResult {
30
+ ok: true;
31
+ action: "monitor-check";
32
+ session_id: string;
33
+ engine: string;
34
+ active_lane_ids: string[];
35
+ covered_lane_ids: string[];
36
+ uncovered_lane_ids: string[];
37
+ skipped?: "engine_has_no_claude_monitor";
38
+ }
39
+ /**
40
+ * Compute the result without the command graph, or `null` when the full
41
+ * command must decide. Exposed for tests that count filesystem calls.
42
+ */
43
+ export declare function computeMonitorCheckFast(request: MonitorCheckFastRequest, deps?: MonitorCheckFastDeps): MonitorCheckFastResult | null;
44
+ /** Returns true when the fast route answered; false means run the full CLI. */
45
+ export declare function runMonitorCheckFast(request: MonitorCheckFastRequest, deps?: MonitorCheckFastDeps): boolean;
46
+ //# sourceMappingURL=lanes-monitor-check-fast.d.ts.map
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Startup fast route for `hq lanes monitor-check`.
3
+ *
4
+ * The Claude Stop hook (`lanes-senior-monitor-stop-gate.sh`) runs this command
5
+ * at the end of every assistant turn in every HQ session, and the
6
+ * SessionStart/UserPromptSubmit shim runs it too. Through the full command
7
+ * graph each call cost ~0.6-0.9 s of CPU, almost all of it module loading
8
+ * (Commander graph, Sentry, undici, the flag-registry worker). With several
9
+ * sessions open that kept one core busy continuously.
10
+ *
11
+ * Nearly every call is for a session that owns no active lane, so the answer
12
+ * is an empty result that needs only the lane-record directory. This module
13
+ * answers that case with `fs` alone. It returns `false` (caller falls back to
14
+ * the full command) whenever the answer could differ from the full path:
15
+ * unusual argv, an unresolvable root, an unreadable store, a conflict sidecar,
16
+ * a record that does not parse, or any record that may name this session as an
17
+ * active senior. The Monitor evidence scan never runs here.
18
+ */
19
+ import * as fs from "node:fs";
20
+ import * as path from "node:path";
21
+ import { resolveLiveRoot } from "./utils/hq-roots-core.js";
22
+ export { parseMonitorCheckFastRequest } from "./lanes-monitor-check-argv.js";
23
+ const ENGINES = new Set(["claude", "codex", "grok"]);
24
+ const ACTIVE_STATES = new Set(["running", "queued", "awaiting_input", "blocked"]);
25
+ /**
26
+ * Compute the result without the command graph, or `null` when the full
27
+ * command must decide. Exposed for tests that count filesystem calls.
28
+ */
29
+ export function computeMonitorCheckFast(request, deps = {}) {
30
+ const env = deps.env ?? process.env;
31
+ const readdirSync = deps.readdirSync ?? ((dir) => fs.readdirSync(dir));
32
+ const readFileSync = deps.readFileSync ?? ((file) => fs.readFileSync(file, "utf8"));
33
+ const engine = request.engine?.trim().toLowerCase() ||
34
+ env.HQ_LANE_ENGINE?.trim().toLowerCase() ||
35
+ env.HQ_CONDUCT_ENGINE?.trim().toLowerCase() ||
36
+ "claude";
37
+ if (!ENGINES.has(engine))
38
+ return null;
39
+ let root;
40
+ try {
41
+ root = resolveLiveRoot({ env, cwd: deps.cwd });
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ const laneDir = path.join(root, "workspace", "lanes", "lanes");
47
+ let names;
48
+ try {
49
+ names = readdirSync(laneDir);
50
+ }
51
+ catch (err) {
52
+ if (err.code !== "ENOENT")
53
+ return null;
54
+ names = [];
55
+ }
56
+ const needle = JSON.stringify(request.sessionId);
57
+ const activeLaneIds = [];
58
+ for (const name of names) {
59
+ if (typeof name !== "string" || name.startsWith("."))
60
+ continue;
61
+ // Sidecars are merged by the full reader; let it decide.
62
+ if (name.includes(".conflict-"))
63
+ return null;
64
+ if (!name.endsWith(".json"))
65
+ continue;
66
+ let text;
67
+ let record;
68
+ try {
69
+ text = readFileSync(path.join(laneDir, name));
70
+ record = JSON.parse(text);
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ if (!record || typeof record !== "object" || Array.isArray(record))
76
+ return null;
77
+ // A record that never mentions the session id cannot belong to it.
78
+ if (!text.includes(needle))
79
+ continue;
80
+ const lane = record;
81
+ const senior = lane.senior;
82
+ if (!senior ||
83
+ typeof senior !== "object" ||
84
+ typeof senior.kind !== "string" ||
85
+ typeof senior.id !== "string" ||
86
+ typeof lane.state !== "string" ||
87
+ typeof lane.lane_id !== "string") {
88
+ return null;
89
+ }
90
+ if (senior.kind === "session" && senior.id === request.sessionId && ACTIVE_STATES.has(lane.state)) {
91
+ // Claude needs the Monitor evidence reader; leave it to the full path.
92
+ if (engine === "claude")
93
+ return null;
94
+ activeLaneIds.push(lane.lane_id);
95
+ }
96
+ }
97
+ const result = {
98
+ ok: true,
99
+ action: "monitor-check",
100
+ session_id: request.sessionId,
101
+ engine,
102
+ active_lane_ids: activeLaneIds,
103
+ covered_lane_ids: [],
104
+ uncovered_lane_ids: [],
105
+ };
106
+ if (engine !== "claude")
107
+ result.skipped = "engine_has_no_claude_monitor";
108
+ return result;
109
+ }
110
+ /** Returns true when the fast route answered; false means run the full CLI. */
111
+ export function runMonitorCheckFast(request, deps = {}) {
112
+ const result = computeMonitorCheckFast(request, deps);
113
+ if (!result)
114
+ return false;
115
+ if (request.json) {
116
+ const write = deps.write ?? ((text) => process.stdout.write(text));
117
+ write(`${JSON.stringify(result, null, 2)}\n`);
118
+ }
119
+ return true;
120
+ }
121
+ //# sourceMappingURL=lanes-monitor-check-fast.js.map
@@ -4,6 +4,7 @@ import * as path from 'node:path';
4
4
  import * as yaml from 'js-yaml';
5
5
  import { contributionLinks } from '../../utils/pack-contribution-links.js';
6
6
  import { CONTRIBUTION_TABLE } from '../../utils/contribution-table.js';
7
+ import { isExcludedWorkerDirectory } from '../workers-registry/index.js';
7
8
  function isDirectory(target) { try {
8
9
  return fs.statSync(target).isDirectory();
9
10
  }
@@ -75,6 +76,11 @@ function workerId(file) {
75
76
  function allWorkerYamlFiles(root, relative) {
76
77
  const output = [];
77
78
  const walk = (next, seen) => {
79
+ // Prune exactly what the worker registry prunes (tenant repos/ checkouts,
80
+ // _template, _overrides). A worker.yaml the registry never indexes cannot
81
+ // collide with a pack worker, and walking source checkouts is slow.
82
+ if (isExcludedWorkerDirectory(next))
83
+ return;
78
84
  const absolute = path.join(root, next);
79
85
  if (!isDirectory(absolute))
80
86
  return;
@@ -9,6 +9,22 @@ export interface GenerateWorkersRegistryResult {
9
9
  written: boolean;
10
10
  quarantined: number;
11
11
  }
12
+ /**
13
+ * Worker roots hold two kinds of `worker.yaml` that must never reach the
14
+ * registry.
15
+ *
16
+ * The `_template` company and any `_overrides` directory are scaffolding —
17
+ * copies kept so a new tenant or a pack override can be stamped out from them.
18
+ *
19
+ * A checkout nested under a tenant's `repos/` is a source repository that tenant
20
+ * happens to keep inside HQ, and when that repository is HQ itself, its own
21
+ * `core/workers` and template trees are full of worker.yaml files. Indexing them
22
+ * treats product source as deployed workers: they claim ids real workers already
23
+ * own, and because duplicates resolve by path order — a tenant path sorts before
24
+ * both `core` and `personal` — the checkout wins. The operator's actual worker
25
+ * then silently vanishes from the registry every skill reads.
26
+ */
27
+ export declare function isExcludedWorkerDirectory(relativeDirectory: string): boolean;
12
28
  /**
13
29
  * Generate core/workers/registry.yaml from worker.yaml files. Invalid workers
14
30
  * are quarantined but do not prevent all valid workers from being registered.
@@ -158,7 +158,7 @@ function withoutTimestamp(content) {
158
158
  * both `core` and `personal` — the checkout wins. The operator's actual worker
159
159
  * then silently vanishes from the registry every skill reads.
160
160
  */
161
- function isExcludedWorkerDirectory(relativeDirectory) {
161
+ export function isExcludedWorkerDirectory(relativeDirectory) {
162
162
  const normalized = relativeDirectory.replaceAll('\\', '/');
163
163
  const [root, tenant, nested] = normalized.split('/');
164
164
  if (root !== 'companies')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.345.115",
3
+ "version": "5.345.117",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {