@indigoai-us/hq-cli 5.320.0 → 5.321.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.321.0] — 2026-10-02
6
+
7
+ - Project catalog cache keys include their source directory, and runtime-probe tests isolate the host CLI and scope checks to hooks.
8
+
9
+ - Fixed intermittent timeouts in the concurrent journal parity test.
10
+ - Fix repo-run-registry so an unremovable stale lock exits with a clear error instead of retrying forever.
11
+ - Added the default-off `hq core hook-batch journal-due` command for one-process PostToolUse journal counter reads.
12
+
5
13
  ## [5.320.0] — 2026-10-02
6
14
  - Write session-journal titles as escaped YAML strings so free-form titles remain parseable.
7
15
  - Added a default-off hq-flags gate that lets lane list pollers skip remote owner refreshes to reduce poll latency.
@@ -96,7 +96,9 @@ _try_lock() {
96
96
  mtime="$(portable_stat_mtime "$LOCK_DIR" 2>/dev/null || echo 0)"
97
97
  age=$(( $(date +%s) - mtime ))
98
98
  if [[ $age -gt 60 ]]; then
99
- rm -rf "$LOCK_DIR" 2>/dev/null || true
99
+ if ! rm -rf "$LOCK_DIR" 2>/dev/null; then
100
+ _die "could not remove stale registry lock: $LOCK_DIR"
101
+ fi
100
102
  continue
101
103
  fi
102
104
  fi
@@ -5971,6 +5971,30 @@ export declare const COMMAND_CATALOG: readonly [{
5971
5971
  }];
5972
5972
  readonly options: readonly [];
5973
5973
  readonly subcommands: readonly [];
5974
+ }, {
5975
+ readonly name: "hook-batch";
5976
+ readonly description: "Batch PostToolUse hook helper calls";
5977
+ readonly aliases: readonly [];
5978
+ readonly hidden: false;
5979
+ readonly usage: "[options] [command]";
5980
+ readonly arguments: readonly [];
5981
+ readonly options: readonly [];
5982
+ readonly subcommands: readonly [{
5983
+ readonly name: "journal-due";
5984
+ readonly description: "Resolve the journal path, increment the tool counter, and return its value";
5985
+ readonly aliases: readonly [];
5986
+ readonly hidden: false;
5987
+ readonly usage: "[options]";
5988
+ readonly arguments: readonly [];
5989
+ readonly options: readonly [{
5990
+ readonly flags: "--session <key>";
5991
+ readonly description: "Session key used by the journal counter";
5992
+ }, {
5993
+ readonly flags: "--reset";
5994
+ readonly description: "Reset the tool counter before incrementing it";
5995
+ }];
5996
+ readonly subcommands: readonly [];
5997
+ }];
5974
5998
  }, {
5975
5999
  readonly name: "session-journal";
5976
6000
  readonly description: "Write and read session journal entries";
@@ -7681,6 +7681,36 @@ export const COMMAND_CATALOG = [
7681
7681
  "options": [],
7682
7682
  "subcommands": []
7683
7683
  },
7684
+ {
7685
+ "name": "hook-batch",
7686
+ "description": "Batch PostToolUse hook helper calls",
7687
+ "aliases": [],
7688
+ "hidden": false,
7689
+ "usage": "[options] [command]",
7690
+ "arguments": [],
7691
+ "options": [],
7692
+ "subcommands": [
7693
+ {
7694
+ "name": "journal-due",
7695
+ "description": "Resolve the journal path, increment the tool counter, and return its value",
7696
+ "aliases": [],
7697
+ "hidden": false,
7698
+ "usage": "[options]",
7699
+ "arguments": [],
7700
+ "options": [
7701
+ {
7702
+ "flags": "--session <key>",
7703
+ "description": "Session key used by the journal counter"
7704
+ },
7705
+ {
7706
+ "flags": "--reset",
7707
+ "description": "Reset the tool counter before incrementing it"
7708
+ }
7709
+ ],
7710
+ "subcommands": []
7711
+ }
7712
+ ]
7713
+ },
7684
7714
  {
7685
7715
  "name": "session-journal",
7686
7716
  "description": "Write and read session journal entries",
@@ -0,0 +1,3 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerCoreHookBatchCommand(core: Command): void;
3
+ //# sourceMappingURL=core-hook-batch-command.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { resolveLiveRoot } from "../utils/hq-roots.js";
2
+ import { describeCoreCommand } from "./core-command-catalog.js";
3
+ import { CORE_HOOK_BATCH_HELP, runCoreHookBatchFast } from "./core-hook-batch-native.js";
4
+ import { coreHookBatchEnabled } from "./core-hook-batch-flag.js";
5
+ export function registerCoreHookBatchCommand(core) {
6
+ const group = core.command("hook-batch")
7
+ .description("Batch PostToolUse hook helper calls");
8
+ const journalDue = group.command("journal-due")
9
+ .description("Resolve the journal path, increment the tool counter, and return its value")
10
+ .requiredOption("--session <key>", "Session key used by the journal counter")
11
+ .option("--reset", "Reset the tool counter before incrementing it")
12
+ .addHelpText("after", `\n${CORE_HOOK_BATCH_HELP}`)
13
+ .action(async (options) => {
14
+ if (!await coreHookBatchEnabled()) {
15
+ process.stderr.write("hook-batch: journal-due requires the core-hook-batch flag\n");
16
+ process.exitCode = 1;
17
+ return;
18
+ }
19
+ const root = resolveLiveRoot({ hqRoot: core.opts().hqRoot });
20
+ const args = ["--session", options.session, ...(options.reset ? ["--reset"] : [])];
21
+ const code = runCoreHookBatchFast(args, "", process.cwd(), root);
22
+ if (code !== 0)
23
+ process.exitCode = code;
24
+ });
25
+ describeCoreCommand(journalDue, { execution: "native", root: "live" });
26
+ }
27
+ //# sourceMappingURL=core-hook-batch-command.js.map
@@ -0,0 +1,4 @@
1
+ /** Default-off gate for the batched PostToolUse helper command. */
2
+ export declare const CORE_HOOK_BATCH_FLAG = "core-hook-batch";
3
+ export declare function coreHookBatchEnabled(): Promise<boolean>;
4
+ //# sourceMappingURL=core-hook-batch-flag.d.ts.map
@@ -0,0 +1,7 @@
1
+ /** Default-off gate for the batched PostToolUse helper command. */
2
+ export const CORE_HOOK_BATCH_FLAG = "core-hook-batch";
3
+ export async function coreHookBatchEnabled() {
4
+ const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
5
+ return resolveProcessFlagGate(CORE_HOOK_BATCH_FLAG, { fallback: false }, () => false);
6
+ }
7
+ //# sourceMappingURL=core-hook-batch-flag.js.map
@@ -0,0 +1,10 @@
1
+ import { type SessionJournalOperationResult } from "./core-session-journal-native.js";
2
+ import type { SessionHelperRuntimeOverrides } from "./core-session-helper-runtime.js";
3
+ export declare const CORE_HOOK_BATCH_HELP = "Usage: hq core hook-batch journal-due --session <key> [--reset]\n\nRuns session-journal dir-path, tool-counter increment, and tool-counter read\ninside one process. With --reset, tool-counter reset runs before increment.\n\nOutput framing: one ASCII line per helper call, in execution order:\n HQHB1|<step>|<exit-code>|<stdout-base64>|<stderr-base64>\nThe delimiter is |; the final newline ends each record. Base64 preserves each\nhelper stream byte-for-byte. In Bash, read each field and decode a stream with\nbase64 --decode (GNU) or base64 -D (BSD); jq is not needed. A helper's code 0\ndoes not imply empty output, and empty streams have empty base64 fields.\n";
4
+ type BatchResult = SessionJournalOperationResult;
5
+ /** Build the no-jq frame over the same in-process operation used by session-journal. */
6
+ export declare function runCoreHookBatch(args: readonly string[], stdin: string, cwd: string, hqRoot: string | undefined, dependencies?: SessionHelperRuntimeOverrides): BatchResult;
7
+ /** Fast-route entrypoint; the encoded records are the only top-level output. */
8
+ export declare function runCoreHookBatchFast(args: readonly string[], stdin: string, cwd: string, hqRoot: string | undefined): number | undefined;
9
+ export {};
10
+ //# sourceMappingURL=core-hook-batch-native.d.ts.map
@@ -0,0 +1,85 @@
1
+ import { runSessionJournalOperation } from "./core-session-journal-native.js";
2
+ export const CORE_HOOK_BATCH_HELP = `Usage: hq core hook-batch journal-due --session <key> [--reset]
3
+
4
+ Runs session-journal dir-path, tool-counter increment, and tool-counter read
5
+ inside one process. With --reset, tool-counter reset runs before increment.
6
+
7
+ Output framing: one ASCII line per helper call, in execution order:
8
+ HQHB1|<step>|<exit-code>|<stdout-base64>|<stderr-base64>
9
+ The delimiter is |; the final newline ends each record. Base64 preserves each
10
+ helper stream byte-for-byte. In Bash, read each field and decode a stream with
11
+ base64 --decode (GNU) or base64 -D (BSD); jq is not needed. A helper's code 0
12
+ does not imply empty output, and empty streams have empty base64 fields.
13
+ `;
14
+ function usageError() {
15
+ return { code: 1, stdout: Buffer.alloc(0), stderr: Buffer.from(CORE_HOOK_BATCH_HELP) };
16
+ }
17
+ function frame(name, result) {
18
+ const line = [
19
+ "HQHB1",
20
+ name,
21
+ String(result.code),
22
+ result.stdout.toString("base64"),
23
+ result.stderr.toString("base64"),
24
+ ].join("|");
25
+ return Buffer.from(`${line}\n`, "ascii");
26
+ }
27
+ function parseArgs(args) {
28
+ if (args.includes("--help") || args.includes("-h"))
29
+ return "help";
30
+ let session;
31
+ let reset = false;
32
+ for (let index = 0; index < args.length; index += 1) {
33
+ const arg = args[index];
34
+ if (arg === "--session") {
35
+ const value = args[index + 1];
36
+ if (!value || value.startsWith("--"))
37
+ return undefined;
38
+ session = value;
39
+ index += 1;
40
+ }
41
+ else if (arg === "--reset") {
42
+ reset = true;
43
+ }
44
+ else {
45
+ return undefined;
46
+ }
47
+ }
48
+ if (!session)
49
+ return undefined;
50
+ return { session, reset };
51
+ }
52
+ function runParsedCoreHookBatch(parsed, stdin, cwd, hqRoot, dependencies) {
53
+ const sessionArgs = ["--session", parsed.session];
54
+ const steps = [
55
+ { name: "dir-path", args: ["dir-path"] },
56
+ ...(parsed.reset ? [{ name: "reset", args: ["tool-counter", "reset", ...sessionArgs] }] : []),
57
+ { name: "increment", args: ["tool-counter", "increment", ...sessionArgs] },
58
+ { name: "read", args: ["tool-counter", "read", ...sessionArgs] },
59
+ ];
60
+ const records = steps.map((step) => {
61
+ const result = runSessionJournalOperation(step.args, stdin, cwd, hqRoot, dependencies);
62
+ return frame(step.name, result);
63
+ });
64
+ return { code: 0, stdout: Buffer.concat(records), stderr: Buffer.alloc(0) };
65
+ }
66
+ /** Build the no-jq frame over the same in-process operation used by session-journal. */
67
+ export function runCoreHookBatch(args, stdin, cwd, hqRoot, dependencies = {}) {
68
+ const parsed = parseArgs(args);
69
+ if (parsed === "help")
70
+ return { code: 0, stdout: Buffer.from(CORE_HOOK_BATCH_HELP), stderr: Buffer.alloc(0) };
71
+ if (!parsed)
72
+ return usageError();
73
+ return runParsedCoreHookBatch(parsed, stdin, cwd, hqRoot, dependencies);
74
+ }
75
+ /** Fast-route entrypoint; the encoded records are the only top-level output. */
76
+ export function runCoreHookBatchFast(args, stdin, cwd, hqRoot) {
77
+ const parsed = parseArgs(args);
78
+ if (!parsed || parsed === "help")
79
+ return undefined;
80
+ const result = runParsedCoreHookBatch(parsed, stdin, cwd, hqRoot, {});
81
+ process.stdout.write(result.stdout);
82
+ process.stderr.write(result.stderr);
83
+ return result.code;
84
+ }
85
+ //# sourceMappingURL=core-hook-batch-native.js.map
@@ -131,7 +131,12 @@ export function runRepoRunRegistry(args, deps) {
131
131
  catch { /* shell falls back to epoch zero when stat fails */ }
132
132
  const age = Math.floor(now().getTime() / 1000) - Math.floor(mtimeMs / 1000);
133
133
  if (age > 60) {
134
- f.rmSync(lockDir, { recursive: true, force: true });
134
+ try {
135
+ f.rmSync(lockDir, { recursive: true, force: true });
136
+ }
137
+ catch {
138
+ throw new Error(`ERROR: could not remove stale registry lock: ${lockDir}`);
139
+ }
135
140
  continue;
136
141
  }
137
142
  }
@@ -1,4 +1,11 @@
1
1
  import { type SessionHelperRuntimeOverrides } from "./core-session-helper-runtime.js";
2
+ export type SessionJournalOperationResult = {
3
+ code: number;
4
+ stdout: Buffer;
5
+ stderr: Buffer;
6
+ };
7
+ /** Execute one native helper call while retaining its two output byte streams. */
8
+ export declare function runSessionJournalOperation(args: readonly string[], _stdin: string, cwd: string, hqRoot: string | undefined, dependencies?: SessionHelperRuntimeOverrides): SessionJournalOperationResult;
2
9
  /** Native implementation of the fail-soft session journal helper. */
3
- export declare function runSessionJournalFast(args: readonly string[], _stdin: string, cwd: string, hqRoot: string | undefined, dependencies?: SessionHelperRuntimeOverrides): number;
10
+ export declare function runSessionJournalFast(args: readonly string[], stdin: string, cwd: string, hqRoot: string | undefined, dependencies?: SessionHelperRuntimeOverrides): number;
4
11
  //# sourceMappingURL=core-session-journal-native.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { resolveSessionHelperRuntime } from "./core-session-helper-runtime.js";
4
+ import { resolveBundledAsset } from "../utils/hq-roots.js";
4
5
  const USAGE = `session-journal.sh: usage:
5
6
  session-journal.sh write "<title>" [--files f1,f2,...] [--body-file PATH] [--session KEY]
6
7
  session-journal.sh list [--date YYYY-MM-DD]
@@ -94,7 +95,7 @@ function acquireJournalWriteLock(dir) {
94
95
  try {
95
96
  fs.writeFileSync(ownerFile, `${process.pid}\n`, { flag: "wx" });
96
97
  }
97
- catch (error) {
98
+ catch {
98
99
  fs.rmSync(lock, { recursive: true, force: true });
99
100
  return undefined;
100
101
  }
@@ -178,7 +179,7 @@ function options(args, valueFlags) {
178
179
  }
179
180
  return values;
180
181
  }
181
- function runToolCounter(root, action, sessionArg, env, now) {
182
+ function runToolCounter(root, action, sessionArg, env, now, writers) {
182
183
  const rawKey = sessionArg || env.HQ_JOURNAL_SESSION || env.HQ_HOOK_SESSION_ID || "";
183
184
  const key = counterKey(rawKey);
184
185
  const dir = journalDir(root, now().toISOString().slice(0, 10));
@@ -188,31 +189,60 @@ function runToolCounter(root, action, sessionArg, env, now) {
188
189
  catch { /* legacy mkdir failure is ignored */ }
189
190
  const counter = path.join(dir, `.tool-count-${key}`);
190
191
  const lock = `${counter}.lock`;
192
+ const reportDirectoryWriteFailure = (error, line) => {
193
+ const code = error.code;
194
+ if (code !== "EISDIR")
195
+ return;
196
+ const asset = resolveBundledAsset("core/scripts/session-journal.sh");
197
+ writers.stderr(`${asset}: line ${line}: ${counter}: Is a directory\n`);
198
+ };
191
199
  if (action === "increment") {
192
200
  withCounterLock(lock, () => {
193
201
  try {
194
202
  fs.writeFileSync(counter, `${readCounter(counter) + 1}\n`);
195
203
  }
196
- catch { /* fail soft */ }
204
+ catch (error) {
205
+ reportDirectoryWriteFailure(error, 312);
206
+ }
197
207
  });
198
208
  }
199
209
  else if (action === "read") {
200
- process.stdout.write(`${readCounter(counter)}\n`);
210
+ writers.stdout(`${readCounter(counter)}\n`);
201
211
  }
202
212
  else if (action === "reset") {
203
213
  withCounterLock(lock, () => {
204
214
  try {
205
215
  fs.writeFileSync(counter, "0\n");
206
216
  }
207
- catch { /* fail soft */ }
217
+ catch (error) {
218
+ reportDirectoryWriteFailure(error, 320);
219
+ }
208
220
  });
209
221
  }
210
222
  else {
211
- warning("tool-counter: subcommand required (increment|read|reset)");
223
+ writers.stderr("session-journal: tool-counter: subcommand required (increment|read|reset)\n");
212
224
  }
213
225
  }
226
+ /** Execute one native helper call while retaining its two output byte streams. */
227
+ export function runSessionJournalOperation(args, _stdin, cwd, hqRoot, dependencies = {}) {
228
+ const stdout = [];
229
+ const stderr = [];
230
+ const writers = {
231
+ stdout: (chunk) => stdout.push(Buffer.from(chunk)),
232
+ stderr: (chunk) => stderr.push(Buffer.from(chunk)),
233
+ };
234
+ const code = executeSessionJournal(args, cwd, hqRoot, dependencies, writers);
235
+ return { code, stdout: Buffer.concat(stdout), stderr: Buffer.concat(stderr) };
236
+ }
214
237
  /** Native implementation of the fail-soft session journal helper. */
215
- export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies = {}) {
238
+ export function runSessionJournalFast(args, stdin, cwd, hqRoot, dependencies = {}) {
239
+ const result = runSessionJournalOperation(args, stdin, cwd, hqRoot, dependencies);
240
+ process.stdout.write(result.stdout);
241
+ process.stderr.write(result.stderr);
242
+ return result.code;
243
+ }
244
+ function executeSessionJournal(args, cwd, hqRoot, dependencies, writers) {
245
+ const warning = (message) => { writers.stderr(`session-journal: ${message}\n`); };
216
246
  const runtime = resolveSessionHelperRuntime(dependencies);
217
247
  const today = () => runtime.now().toISOString().slice(0, 10);
218
248
  const nowIso = () => runtime.now().toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -296,8 +326,8 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
296
326
  finally {
297
327
  releaseJournalWriteLock(writeLock);
298
328
  }
299
- runToolCounter(root, "reset", counterKey(session || runtime.env.HQ_JOURNAL_SESSION || runtime.env.HQ_HOOK_SESSION_ID || ""), runtime.env, runtime.now);
300
- process.stdout.write(`${output}\n`);
329
+ runToolCounter(root, "reset", counterKey(session || runtime.env.HQ_JOURNAL_SESSION || runtime.env.HQ_HOOK_SESSION_ID || ""), runtime.env, runtime.now, writers);
330
+ writers.stdout(`${output}\n`);
301
331
  return 0;
302
332
  }
303
333
  if (command === "list" || command === "read" || command === "index-path" || command === "dir-path") {
@@ -306,19 +336,19 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
306
336
  const date = options(afterSequence, ["--date"]).get("--date") || today();
307
337
  const dir = journalDir(root, date);
308
338
  if (command === "dir-path") {
309
- process.stdout.write(dir);
339
+ writers.stdout(dir);
310
340
  return 0;
311
341
  }
312
342
  if (command === "index-path") {
313
- process.stdout.write(`${dir}/INDEX.md\n`);
343
+ writers.stdout(`${dir}/INDEX.md\n`);
314
344
  return 0;
315
345
  }
316
346
  if (command === "list") {
317
347
  const index = path.join(dir, "INDEX.md");
318
348
  if (hasFile(index))
319
- process.stdout.write(fs.readFileSync(index));
349
+ writers.stdout(fs.readFileSync(index));
320
350
  else
321
- process.stdout.write(`(no journal for ${date})\n`);
351
+ writers.stdout(`(no journal for ${date})\n`);
322
352
  return 0;
323
353
  }
324
354
  if (!sequence) {
@@ -339,7 +369,7 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
339
369
  }
340
370
  })();
341
371
  if (match)
342
- process.stdout.write(fs.readFileSync(path.join(dir, match)));
372
+ writers.stdout(fs.readFileSync(path.join(dir, match)));
343
373
  else
344
374
  warning(`read: no entry ${normalized} for ${date}`);
345
375
  return 0;
@@ -347,10 +377,10 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
347
377
  if (command === "tool-counter") {
348
378
  const action = rest[0] ?? "";
349
379
  const session = options(rest.slice(1), ["--session"]).get("--session") ?? "";
350
- runToolCounter(root, action, session, runtime.env, runtime.now);
380
+ runToolCounter(root, action, session, runtime.env, runtime.now, writers);
351
381
  return 0;
352
382
  }
353
- process.stderr.write(USAGE);
383
+ writers.stderr(USAGE);
354
384
  return 1;
355
385
  }
356
386
  //# sourceMappingURL=core-session-journal-native.js.map
@@ -47,6 +47,7 @@ import { registerCoreRefreshVaultAccessNative } from "./core-refresh-vault-acces
47
47
  import { registerCoreWorkMeshBindTrustedNative } from "./core-work-mesh-bind-trusted-native.js";
48
48
  import { registerCoreHookHelperNativeCommands } from "./core-native-hook-helper-commands.js";
49
49
  import { registerCoreHookStateHelpers } from "./core-hook-state-helpers-native.js";
50
+ import { registerCoreHookBatchCommand } from "./core-hook-batch-command.js";
50
51
  import { resolveLiveRoot } from "../utils/hq-roots.js";
51
52
  import { interpreterFor, runBundledScript } from "../utils/run-bundled-script.js";
52
53
  import { CompanyIndexUserError, renderIndexTarget } from "../lib/index-render/index.js";
@@ -397,6 +398,7 @@ export function registerCoreCommands(program) {
397
398
  registerCoreWorkMeshBindTrustedNative(core);
398
399
  registerCoreHookHelperNativeCommands(core);
399
400
  registerCoreHookStateHelpers(core);
401
+ registerCoreHookBatchCommand(core);
400
402
  const sessionHelperCommands = [
401
403
  {
402
404
  name: "session-journal", asset: "core/scripts/session-journal.sh", root: "live",
package/dist/fast-core.js CHANGED
@@ -35,6 +35,11 @@ export async function runFastCore(argv) {
35
35
  const envOverride = localBooleanFlagOverride(entry.familyFlagKey, process.env);
36
36
  familyEnabled = envOverride ?? await (await import("./native-fast-route.js")).fastFlagEnabled(entry.familyFlagKey, process.env, { numericEnvironmentOverride: false });
37
37
  }
38
+ if (entry.kind === "native" && entry.fallbackToCommandGraph && !familyEnabled) {
39
+ const { runCli } = await import("./main.js");
40
+ await runCli();
41
+ return;
42
+ }
38
43
  const useShell = entry.kind === "native" && Boolean(entry.familyFlagKey) && !familyEnabled;
39
44
  const worktreeScopeFixEnabled = entry.name === "repo-run-registry"
40
45
  ? await (await import("./native-fast-route.js")).fastFlagEnabled(CORE_RRR_WORKTREE_SCOPE_FIX_FLAG, process.env, { numericEnvironmentOverride: false })
@@ -74,6 +79,14 @@ export async function runFastCore(argv) {
74
79
  const readsStdin = entry.readsStdinFor?.(parsed.args) ?? entry.readsStdin ?? false;
75
80
  const stdin = readsStdin ? await readStdinIfPiped(process.stdin) : "";
76
81
  const code = await handler(parsed.args, stdin, process.cwd(), hqRoot, entry.name.split(" ").at(-1) ?? entry.name, parsed.hqRootArg, worktreeScopeFixEnabled);
82
+ if (code === undefined) {
83
+ if (!entry.fallbackToCommandGraph) {
84
+ throw new Error(`Native fast handler ${entry.exportName} requested an unavailable command-graph fallback`);
85
+ }
86
+ const { runCli } = await import("./main.js");
87
+ await runCli();
88
+ return;
89
+ }
77
90
  // Let Node drain handler output before terminating. The timeout guard's
78
91
  // stderr message must survive the native route's process boundary.
79
92
  process.exitCode = code;
@@ -32,6 +32,8 @@ export type FastScaffoldScript = {
32
32
  familyFlagKey?: string;
33
33
  /** Shell rollback companion used while the family gate is off. */
34
34
  asset?: string;
35
+ /** Return to the Commander graph when this new route has no shell companion. */
36
+ fallbackToCommandGraph?: boolean;
35
37
  /** Root context used by the native handler. */
36
38
  root?: "live" | "cwd" | "passthrough";
37
39
  /** Cwd used for a shell rollback companion. Defaults to the caller cwd. */
@@ -217,6 +217,16 @@ export const FAST_SCAFFOLD_SCRIPTS = [
217
217
  shellHqRoot: "live",
218
218
  ...(name === "session-project" ? { readsStdin: true, readsStdinFor: (args) => args[0] === "ingest-plan" } : {}),
219
219
  })),
220
+ {
221
+ name: "hook-batch journal-due",
222
+ kind: "native",
223
+ modulePath: "./commands/core-hook-batch-native.js",
224
+ exportName: "runCoreHookBatchFast",
225
+ flagKey: "hq-core-native-fast-route",
226
+ familyFlagKey: "core-hook-batch",
227
+ root: "live",
228
+ fallbackToCommandGraph: true,
229
+ },
220
230
  ...[
221
231
  { name: "eval-trigger", modulePath: "./commands/core-eval-trigger-native.js", exportName: "runEvalTriggerFast", asset: "core/scripts/eval-trigger.sh", root: "cwd", shellCwd: "caller", shellHqRoot: "none", readsStdinFor: (args) => args[0] === "--check" },
222
232
  { name: "migrate-policy-triggers", modulePath: "./commands/core-migrate-policy-triggers-native.js", exportName: "runMigratePolicyTriggersFast", asset: "core/scripts/migrate-policy-triggers.sh", root: "live", shellCwd: "caller", shellHqRoot: "live" },
@@ -41,7 +41,7 @@ export declare function discoverProjects(input: {
41
41
  env?: NodeJS.ProcessEnv;
42
42
  home?: string;
43
43
  }): CatalogProject[];
44
- /** discoverProjects memoized by directory mtime. A cache hit does not rescan. */
44
+ /** discoverProjects memoized by source directory and mtime. A hit does not rescan. */
45
45
  export declare function discoverProjectsCached(input: Parameters<typeof discoverProjects>[0], nowMs?: number): CatalogProject[];
46
46
  export declare function refineProjectBinding(input: {
47
47
  hint?: string;
@@ -215,14 +215,14 @@ function catalogStamp(input) {
215
215
  if (!dir)
216
216
  return "0";
217
217
  try {
218
- return String(fs.statSync(dir).mtimeMs);
218
+ return `${dir}:${fs.statSync(dir).mtimeMs}`;
219
219
  }
220
220
  catch {
221
- return "0";
221
+ return `${dir}:0`;
222
222
  }
223
223
  }).join("|");
224
224
  }
225
- /** discoverProjects memoized by directory mtime. A cache hit does not rescan. */
225
+ /** discoverProjects memoized by source directory and mtime. A hit does not rescan. */
226
226
  export function discoverProjectsCached(input, nowMs = Date.now()) {
227
227
  const key = `${input.hqRoot ?? ""}|${input.companySlug ?? ""}|${input.companyUid ?? ""}|${input.home ?? ""}|${catalogStamp(input)}`;
228
228
  if (catalogCache && catalogCache.key === key && nowMs - catalogCache.at < CATALOG_CACHE_TTL_MS) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.320.0",
3
+ "version": "5.321.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {