@indigoai-us/hq-cli 5.345.116 → 5.345.118
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 +11 -0
- package/dist/commands/integrations-core.d.ts +7 -1
- package/dist/commands/integrations-core.js +17 -4
- package/dist/commands/integrations.js +91 -22
- package/dist/index.js +17 -0
- package/dist/lanes-monitor-check-argv.d.ts +12 -0
- package/dist/lanes-monitor-check-argv.js +54 -0
- package/dist/lanes-monitor-check-fast.d.ts +46 -0
- package/dist/lanes-monitor-check-fast.js +121 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.345.118] — 2026-10-13
|
|
6
|
+
- `hq integrations call` and `hq integrations tools` now work for the
|
|
7
|
+
built-in Google Workspace, Linear and Slack connections. Calls use the
|
|
8
|
+
tool's own name (for example `google.drive.picked.list`), and `tools`
|
|
9
|
+
lists only the selected connection's tools. `--args` may not set
|
|
10
|
+
`companyUid`, `connectionId` or `idempotencyKey`; hq fills those in from
|
|
11
|
+
the selected connection.
|
|
12
|
+
|
|
13
|
+
## [5.345.117] — 2026-10-13
|
|
14
|
+
- `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.
|
|
15
|
+
|
|
5
16
|
## [5.345.116] — 2026-10-13
|
|
6
17
|
|
|
7
18
|
## [5.345.115] — 2026-10-13
|
|
@@ -257,6 +257,12 @@ export interface UsableAppsResult {
|
|
|
257
257
|
viewerAccessKnown: boolean;
|
|
258
258
|
apps: UsableApp[];
|
|
259
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Built-in providers expose their tools under their own names
|
|
262
|
+
* (`google.drive.picked.list`), not through the factory passthrough
|
|
263
|
+
* `<prefix>.mcp.tools.call`, which the gateway never registers for them.
|
|
264
|
+
*/
|
|
265
|
+
export declare function isBuiltInGatewayProvider(provider: string): boolean;
|
|
260
266
|
/**
|
|
261
267
|
* The apps the caller may use, from `GET /v1/integrations/admin?view=summary`.
|
|
262
268
|
* A row counts only when all of these hold, mirroring the gateway's checks:
|
|
@@ -296,7 +302,7 @@ export declare function resolveConnection(token: string, companyUid: string, app
|
|
|
296
302
|
}, recoveryOpts?: {
|
|
297
303
|
allowSingleRevoked?: boolean;
|
|
298
304
|
}): Promise<AdminConnection>;
|
|
299
|
-
export declare function callGateway(token: string, params: Record<string, unknown>, idempotencyKey?: string): Promise<GatewayMessage>;
|
|
305
|
+
export declare function callGateway(token: string, params: Record<string, unknown>, idempotencyKey?: string, method?: "tools/call" | "tools/list"): Promise<GatewayMessage>;
|
|
300
306
|
/**
|
|
301
307
|
* Gateway results arrive MCP-style: `{ content: [{ type: "text", text }] }`
|
|
302
308
|
* where `text` is the provider's JSON. Unwrap to the inner payload; fall back
|
|
@@ -368,6 +368,14 @@ const GATEWAY_SERVED_STATUSES = new Set(["connected", "needs-attention"]);
|
|
|
368
368
|
* gateway as `ProviderUnsupported`.
|
|
369
369
|
*/
|
|
370
370
|
const STATIC_GATEWAY_PROVIDERS = new Set(["google-workspace", "linear", "slack"]);
|
|
371
|
+
/**
|
|
372
|
+
* Built-in providers expose their tools under their own names
|
|
373
|
+
* (`google.drive.picked.list`), not through the factory passthrough
|
|
374
|
+
* `<prefix>.mcp.tools.call`, which the gateway never registers for them.
|
|
375
|
+
*/
|
|
376
|
+
export function isBuiltInGatewayProvider(provider) {
|
|
377
|
+
return STATIC_GATEWAY_PROVIDERS.has(provider);
|
|
378
|
+
}
|
|
371
379
|
const MAX_APP_NAME_LENGTH = 60;
|
|
372
380
|
/**
|
|
373
381
|
* Display names are set by whoever installed the app and end up in agent
|
|
@@ -561,7 +569,9 @@ export async function resolveConnection(token, companyUid, app, opts, recoveryOp
|
|
|
561
569
|
}
|
|
562
570
|
return selectConnection(connections, opts);
|
|
563
571
|
}
|
|
564
|
-
function gatewaySubcommandFor(params) {
|
|
572
|
+
function gatewaySubcommandFor(params, method) {
|
|
573
|
+
if (method === "tools/list")
|
|
574
|
+
return "tools_list";
|
|
565
575
|
const name = params.name;
|
|
566
576
|
if (typeof name !== "string")
|
|
567
577
|
return "other";
|
|
@@ -569,10 +579,13 @@ function gatewaySubcommandFor(params) {
|
|
|
569
579
|
return "tools_list";
|
|
570
580
|
if (name.endsWith(".mcp.tools.call"))
|
|
571
581
|
return "tools_call";
|
|
582
|
+
// Built-in providers are called by the tool's own name, with no passthrough.
|
|
583
|
+
if (!name.includes(".mcp."))
|
|
584
|
+
return "tools_call";
|
|
572
585
|
return "other";
|
|
573
586
|
}
|
|
574
|
-
export async function callGateway(token, params, idempotencyKey) {
|
|
575
|
-
const gatewaySubcommand = gatewaySubcommandFor(params);
|
|
587
|
+
export async function callGateway(token, params, idempotencyKey, method = "tools/call") {
|
|
588
|
+
const gatewaySubcommand = gatewaySubcommandFor(params, method);
|
|
576
589
|
const deadline = Date.now() + ASYNC_GATEWAY_DEADLINE_MS;
|
|
577
590
|
const res = await vaultApiFetch({
|
|
578
591
|
token,
|
|
@@ -581,7 +594,7 @@ export async function callGateway(token, params, idempotencyKey) {
|
|
|
581
594
|
body: {
|
|
582
595
|
jsonrpc: "2.0",
|
|
583
596
|
id: `hq-cli-${randomUUID()}`,
|
|
584
|
-
method
|
|
597
|
+
method,
|
|
585
598
|
params,
|
|
586
599
|
},
|
|
587
600
|
});
|
|
@@ -54,12 +54,39 @@ import chalk from "chalk";
|
|
|
54
54
|
import { ensureCognitoIdToken } from "../utils/cognito-session.js";
|
|
55
55
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
56
56
|
import { redactSensitiveUrl, redactSensitiveUrls, redactUrlsInText } from "../utils/redact-url.js";
|
|
57
|
-
import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, fetchUsableApps, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
|
|
57
|
+
import { IntegrationsCliError, bareProvider, callGateway, fetchConnections, fetchUsableApps, isBuiltInGatewayProvider, isClientError, printHealthNotice, printJson, printIntegrationCallResultJson, raiseIfUnauthorized, raiseIfUpstreamUnavailable, gatewayResultIsError, selectConnection, revokedConnectionDetails, toolPrefixForProvider, unwrapGatewayResult, queuedOutcome, } from "./integrations-core.js";
|
|
58
58
|
import { registerConnectCommands } from "./integrations-connect.js";
|
|
59
59
|
import { registerImportCommands } from "./integrations-import.js";
|
|
60
60
|
import { registerManageCommands } from "./integrations-manage.js";
|
|
61
61
|
import { registerSlackCommands } from "./integrations-slack.js";
|
|
62
62
|
import { fetchPendingApprovals } from "./integrations-api.js";
|
|
63
|
+
/** Routing ids hq sets on a built-in tool call; never accepted from --args. */
|
|
64
|
+
const BUILT_IN_ROUTING_ARGS = ["companyUid", "connectionId", "idempotencyKey"];
|
|
65
|
+
/**
|
|
66
|
+
* hq sets the routing ids on every built-in call and refuses them in --args,
|
|
67
|
+
* so discovery must not list them as inputs the caller provides.
|
|
68
|
+
*/
|
|
69
|
+
function withoutRoutingArgs(tool) {
|
|
70
|
+
const schema = tool.inputSchema;
|
|
71
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema))
|
|
72
|
+
return tool;
|
|
73
|
+
const { properties, required, ...rest } = schema;
|
|
74
|
+
const routing = new Set(BUILT_IN_ROUTING_ARGS);
|
|
75
|
+
const next = { ...rest };
|
|
76
|
+
if (properties && typeof properties === "object" && !Array.isArray(properties)) {
|
|
77
|
+
next.properties = Object.fromEntries(Object.entries(properties).filter(([key]) => !routing.has(key)));
|
|
78
|
+
}
|
|
79
|
+
else if (properties !== undefined) {
|
|
80
|
+
next.properties = properties;
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(required)) {
|
|
83
|
+
next.required = required.filter((key) => !(typeof key === "string" && routing.has(key)));
|
|
84
|
+
}
|
|
85
|
+
else if (required !== undefined) {
|
|
86
|
+
next.required = required;
|
|
87
|
+
}
|
|
88
|
+
return { ...tool, inputSchema: next };
|
|
89
|
+
}
|
|
63
90
|
export { IntegrationsCliError, callGateway, fetchConnections, fetchUsableApps, queuedOutcome, selectConnection, toolPrefixForProvider, unwrapGatewayResult, } from "./integrations-core.js";
|
|
64
91
|
/**
|
|
65
92
|
* Gateway metadata is additive and may be absent when talking to an older
|
|
@@ -285,16 +312,33 @@ export function registerIntegrationsCommand(program) {
|
|
|
285
312
|
.action(async (opts) => {
|
|
286
313
|
const token = await ensureCognitoIdToken();
|
|
287
314
|
const companyUid = await getCompanyUid(token, opts.company);
|
|
288
|
-
const
|
|
315
|
+
const connections = await fetchConnections(token, companyUid);
|
|
316
|
+
const connection = selectConnection(connections, opts);
|
|
289
317
|
if (reportRevokedConnection(connection, opts))
|
|
290
318
|
return;
|
|
291
|
-
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
319
|
+
let payload;
|
|
320
|
+
const builtIn = isBuiltInGatewayProvider(connection.provider);
|
|
321
|
+
if (builtIn) {
|
|
322
|
+
// Built-in providers answer the gateway's own tools/list, which spans
|
|
323
|
+
// every connection in the company; keep only this connection's tools.
|
|
324
|
+
const message = await callGateway(token, { companyUid }, undefined, "tools/list");
|
|
325
|
+
const listed = unwrapGatewayResult(message.result);
|
|
326
|
+
payload = {
|
|
327
|
+
...listed,
|
|
328
|
+
tools: (listed?.tools ?? [])
|
|
329
|
+
.filter((tool) => tool.annotations?.connectionId === connection.id)
|
|
330
|
+
.map(withoutRoutingArgs),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
const prefix = toolPrefixForProvider(connection.provider);
|
|
335
|
+
const message = await callGateway(token, {
|
|
336
|
+
companyUid,
|
|
337
|
+
name: `${prefix}.mcp.tools.list`,
|
|
338
|
+
arguments: { companyUid, connectionId: connection.id },
|
|
339
|
+
});
|
|
340
|
+
payload = unwrapGatewayResult(message.result);
|
|
341
|
+
}
|
|
298
342
|
const tools = payload?.tools ?? [];
|
|
299
343
|
if (tools.length === 0) {
|
|
300
344
|
if (opts.json) {
|
|
@@ -331,7 +375,14 @@ export function registerIntegrationsCommand(program) {
|
|
|
331
375
|
const badge = formatToolBadge(tool);
|
|
332
376
|
console.log(`${chalk.bold(tool.name)}${badge ? ` ${badge}` : ""}${label}${required ? ` ${chalk.dim(required)}` : ""}`);
|
|
333
377
|
}
|
|
334
|
-
|
|
378
|
+
// `--provider` resolves to the first matching connection, so when
|
|
379
|
+
// several connections share this provider the hint must name this one.
|
|
380
|
+
const sharedProvider = builtIn &&
|
|
381
|
+
connections.some((c) => c.id !== connection.id && c.status !== "revoked" && c.provider === connection.provider);
|
|
382
|
+
const selector = sharedProvider
|
|
383
|
+
? `--connection ${connection.id}`
|
|
384
|
+
: `--provider ${bareProvider(connection.provider)}`;
|
|
385
|
+
console.log(chalk.dim(`\n${tools.length} tools. Describe inputs: hq integrations tools ${selector} --describe <tool>\nCall one with: hq integrations call <tool> ${selector} --args '<json>'`));
|
|
335
386
|
});
|
|
336
387
|
integrations
|
|
337
388
|
.command("call <tool>")
|
|
@@ -359,20 +410,38 @@ export function registerIntegrationsCommand(program) {
|
|
|
359
410
|
const connection = selectConnection(await fetchConnections(token, companyUid), opts);
|
|
360
411
|
if (reportRevokedConnection(connection, { ...opts, fail: true }))
|
|
361
412
|
return;
|
|
362
|
-
const prefix = toolPrefixForProvider(connection.provider);
|
|
363
413
|
const idempotencyKey = opts.idempotencyKey ?? `hq-cli-${randomUUID()}`;
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
name
|
|
368
|
-
|
|
414
|
+
let gatewayParams;
|
|
415
|
+
if (isBuiltInGatewayProvider(connection.provider)) {
|
|
416
|
+
// Built-in tools take their args flat beside the routing ids, so an
|
|
417
|
+
// arg with a routing name would redirect the call; refuse it.
|
|
418
|
+
const reserved = BUILT_IN_ROUTING_ARGS.filter((key) => key in parsedArgs);
|
|
419
|
+
if (reserved.length) {
|
|
420
|
+
throw new IntegrationsCliError(`--args cannot include ${reserved.join(", ")}: it is set by hq from the selected connection.`, { expected: true });
|
|
421
|
+
}
|
|
422
|
+
gatewayParams = {
|
|
423
|
+
companyUid,
|
|
424
|
+
acceptAsync: true,
|
|
425
|
+
name: tool,
|
|
426
|
+
arguments: { ...parsedArgs, companyUid, connectionId: connection.id, idempotencyKey },
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
const prefix = toolPrefixForProvider(connection.provider);
|
|
431
|
+
gatewayParams = {
|
|
369
432
|
companyUid,
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
arguments:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
433
|
+
acceptAsync: true,
|
|
434
|
+
name: `${prefix}.mcp.tools.call`,
|
|
435
|
+
arguments: {
|
|
436
|
+
companyUid,
|
|
437
|
+
connectionId: connection.id,
|
|
438
|
+
toolName: tool,
|
|
439
|
+
arguments: parsedArgs,
|
|
440
|
+
idempotencyKey,
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
const message = await callGateway(token, gatewayParams, idempotencyKey);
|
|
376
445
|
const payload = unwrapGatewayResult(message.result);
|
|
377
446
|
const queued = queuedOutcome(payload);
|
|
378
447
|
if (queued) {
|
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
|