@indigoai-us/hq-cli 5.319.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/assets/scaffold/core/scripts/repo-run-registry.sh +3 -1
  3. package/assets/scaffold/core/scripts/session-journal.sh +40 -2
  4. package/dist/bin/hq-auth-refresh.d.ts +3 -1
  5. package/dist/bin/hq-auth-refresh.js +41 -6
  6. package/dist/command-catalog.generated.d.ts +24 -0
  7. package/dist/command-catalog.generated.js +30 -0
  8. package/dist/commands/core-hook-batch-command.d.ts +3 -0
  9. package/dist/commands/core-hook-batch-command.js +27 -0
  10. package/dist/commands/core-hook-batch-flag.d.ts +4 -0
  11. package/dist/commands/core-hook-batch-flag.js +7 -0
  12. package/dist/commands/core-hook-batch-native.d.ts +10 -0
  13. package/dist/commands/core-hook-batch-native.js +85 -0
  14. package/dist/commands/core-repo-run-registry-native.js +6 -1
  15. package/dist/commands/core-session-journal-native.d.ts +8 -1
  16. package/dist/commands/core-session-journal-native.js +77 -18
  17. package/dist/commands/core.js +2 -0
  18. package/dist/commands/lanes.js +28 -5
  19. package/dist/fast-core.js +13 -0
  20. package/dist/fast-scaffold.d.ts +2 -0
  21. package/dist/fast-scaffold.js +10 -0
  22. package/dist/index.d.ts +9 -0
  23. package/dist/index.js +135 -63
  24. package/dist/lib/daemon/run.d.ts +2 -0
  25. package/dist/lib/daemon/run.js +10 -1
  26. package/dist/lib/lanes/flag-keys.d.ts +3 -0
  27. package/dist/lib/lanes/flag-keys.js +3 -0
  28. package/dist/lib/work-context/bind-best.d.ts +1 -1
  29. package/dist/lib/work-context/bind-best.js +3 -3
  30. package/dist/utils/package-use-lease.d.ts +25 -0
  31. package/dist/utils/package-use-lease.js +213 -0
  32. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
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
+
13
+ ## [5.320.0] — 2026-10-02
14
+ - Write session-journal titles as escaped YAML strings so free-form titles remain parseable.
15
+ - Added a default-off hq-flags gate that lets lane list pollers skip remote owner refreshes to reduce poll latency.
16
+ - Publish per-process package-use leases so Desktop updates can wait for active hq-cli work without native locks.
17
+
5
18
  ## [5.319.0] — 2026-10-02
6
19
  - Let `hq access` use ACL-checked company presigned reads when STS vend rejects a truncated write-policy scope.
7
20
  - Lead autopilot on an Outpost can wake leads that were created on that Outpost, behind the default-off `lanes.autopilot-outpost-native-leads` flag. Before, those leads were skipped as a host mismatch and got no wakes.
@@ -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
@@ -43,6 +43,41 @@ slugify() {
43
43
  | cut -c1-40
44
44
  }
45
45
 
46
+ yaml_double_quoted() {
47
+ local value="$1" output='"' character next codepoint hex
48
+ local LC_ALL=C index
49
+ for ((index = 0; index < ${#value}; index++)); do
50
+ character="${value:index:1}"
51
+ if [[ "$character" == $'\xc2' ]] && (( index + 1 < ${#value} )); then
52
+ next="${value:index+1:1}"
53
+ printf -v codepoint '%d' "'${next}"
54
+ if (( codepoint >= 128 && codepoint <= 159 )); then
55
+ printf -v hex '%04x' "$codepoint"
56
+ output+="\\u$hex"
57
+ index=$((index + 1))
58
+ continue
59
+ fi
60
+ fi
61
+ case "$character" in
62
+ \\) output+='\\' ;;
63
+ '"') output+='\"' ;;
64
+ $'\n') output+='\n' ;;
65
+ $'\r') output+='\r' ;;
66
+ $'\t') output+='\t' ;;
67
+ *)
68
+ if [[ "$character" == [[:cntrl:]] ]]; then
69
+ printf -v codepoint '%d' "'${character}"
70
+ printf -v hex '%02x' "$codepoint"
71
+ output+="\\x$hex"
72
+ else
73
+ output+="$character"
74
+ fi
75
+ ;;
76
+ esac
77
+ done
78
+ printf '%s"' "$output"
79
+ }
80
+
46
81
  # Counters are per-session. A date-global counter lets concurrent Claude and
47
82
  # Codex tasks trip (and reset) each other's journal reminders.
48
83
  counter_key() {
@@ -140,6 +175,9 @@ case "$cmd" in
140
175
 
141
176
  slug="$(slugify "$title")"
142
177
  [ -z "$slug" ] && slug="entry"
178
+ yaml_title="$(yaml_double_quoted "$title")"
179
+ yaml_slug="$slug"
180
+ [[ "$slug" != *$'\n'* ]] || yaml_slug="$(yaml_double_quoted "$slug")"
143
181
  JOURNAL_WRITE_LOCK=""
144
182
  acquire_journal_write_lock "$dir" || { warn "write: failed to acquire journal lock"; exit 0; }
145
183
  trap release_journal_write_lock EXIT
@@ -161,8 +199,8 @@ case "$cmd" in
161
199
  {
162
200
  printf -- '---\n'
163
201
  printf 'ts: %s\n' "$(now_iso_z)"
164
- printf 'title: %s\n' "$title"
165
- printf 'slug: %s\n' "$slug"
202
+ printf 'title: %s\n' "$yaml_title"
203
+ printf 'slug: %s\n' "$yaml_slug"
166
204
  if [ -n "$files_csv" ]; then
167
205
  printf 'files:\n'
168
206
  old_ifs="$IFS"
@@ -11,7 +11,9 @@
11
11
  * non-interactively.
12
12
  */
13
13
  import "../node-preflight.js";
14
+ import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
14
15
  import "../node-network-compat.js";
15
- import { refreshCachedSession } from "../utils/cognito-session.js";
16
+ import type { refreshCachedSession } from "../utils/cognito-session.js";
16
17
  export declare function runAuthRefresh(refreshSession?: typeof refreshCachedSession): Promise<number>;
18
+ export declare function startAuthRefresh(acquireLease?: typeof acquireCurrentPackageUseLease): Promise<number>;
17
19
  //# sourceMappingURL=hq-auth-refresh.d.ts.map
@@ -13,15 +13,16 @@
13
13
  // MUST be first: guard the Node version before any dependency that needs a
14
14
  // Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
15
15
  import "../node-preflight.js";
16
+ import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
16
17
  import "../node-network-compat.js";
17
18
  import { fileURLToPath } from "node:url";
18
19
  import path from "node:path";
19
- import { finishSentrySession, initSentry, Sentry } from "../sentry.js";
20
- import { refreshCachedSession } from "../utils/cognito-session.js";
21
- export async function runAuthRefresh(refreshSession = refreshCachedSession) {
20
+ export async function runAuthRefresh(refreshSession) {
21
+ const { finishSentrySession, Sentry } = await import("../sentry.js");
22
+ const { refreshCachedSession: refresh } = await import("../utils/cognito-session.js");
22
23
  let exitCode = 1;
23
24
  try {
24
- const result = await refreshSession();
25
+ const result = await (refreshSession ?? refresh)();
25
26
  if (result.refreshed) {
26
27
  exitCode = 0;
27
28
  }
@@ -41,8 +42,42 @@ export async function runAuthRefresh(refreshSession = refreshCachedSession) {
41
42
  }
42
43
  return exitCode;
43
44
  }
44
- if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
45
+ function isPackageUseUpdatePendingError(error) {
46
+ return Boolean(error &&
47
+ typeof error === "object" &&
48
+ error.name === "PackageUseUpdatePendingError");
49
+ }
50
+ function packageUseErrorClass(error) {
51
+ if (!error || typeof error !== "object")
52
+ return "unknown";
53
+ const value = error;
54
+ if (typeof value.code === "string")
55
+ return value.code;
56
+ if (value.cause && typeof value.cause === "object") {
57
+ const cause = value.cause;
58
+ if (typeof cause.code === "string")
59
+ return cause.code;
60
+ if (typeof cause.name === "string")
61
+ return cause.name;
62
+ }
63
+ return typeof value.name === "string" ? value.name : "unknown";
64
+ }
65
+ export async function startAuthRefresh(acquireLease = acquireCurrentPackageUseLease) {
66
+ try {
67
+ acquireLease();
68
+ }
69
+ catch (error) {
70
+ if (isPackageUseUpdatePendingError(error)) {
71
+ process.stderr.write(`hq-auth-refresh: ${error instanceof Error ? error.message : "The HQ CLI package is being updated"}\n`);
72
+ return 1;
73
+ }
74
+ process.stderr.write(`hq-auth-refresh: package-use lease unavailable (${packageUseErrorClass(error)}); continuing without lease\n`);
75
+ }
76
+ const { initSentry } = await import("../sentry.js");
45
77
  initSentry();
46
- void runAuthRefresh().then((exitCode) => process.exit(exitCode));
78
+ return runAuthRefresh();
79
+ }
80
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
81
+ void startAuthRefresh().then((exitCode) => process.exit(exitCode));
47
82
  }
48
83
  //# sourceMappingURL=hq-auth-refresh.js.map
@@ -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]
@@ -24,7 +25,35 @@ const counterKey = (value) => {
24
25
  return key.length > 96 ? key.slice(0, 96) : key;
25
26
  };
26
27
  const slugify = (value) => value.toLowerCase()
27
- .replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 40);
28
+ .split("\n")
29
+ .map((line) => line.replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 40))
30
+ .join("\n");
31
+ function yamlDoubleQuoted(value) {
32
+ let escaped = "";
33
+ for (const character of value) {
34
+ const codePoint = character.codePointAt(0) ?? 0;
35
+ if (character === "\\")
36
+ escaped += "\\\\";
37
+ else if (character === '"')
38
+ escaped += '\\"';
39
+ else if (character === "\n")
40
+ escaped += "\\n";
41
+ else if (character === "\r")
42
+ escaped += "\\r";
43
+ else if (character === "\t")
44
+ escaped += "\\t";
45
+ else if (codePoint < 0x20 || codePoint === 0x7f) {
46
+ escaped += `\\x${codePoint.toString(16).padStart(2, "0")}`;
47
+ }
48
+ else if (codePoint >= 0x80 && codePoint <= 0x9f) {
49
+ escaped += `\\u${codePoint.toString(16).padStart(4, "0")}`;
50
+ }
51
+ else {
52
+ escaped += character;
53
+ }
54
+ }
55
+ return `"${escaped}"`;
56
+ }
28
57
  const warning = (message) => { process.stderr.write(`session-journal: ${message}\n`); };
29
58
  const journalDir = (root, date) => path.join(root, "workspace/threads/journal", date);
30
59
  const hasFile = (file) => {
@@ -66,7 +95,7 @@ function acquireJournalWriteLock(dir) {
66
95
  try {
67
96
  fs.writeFileSync(ownerFile, `${process.pid}\n`, { flag: "wx" });
68
97
  }
69
- catch (error) {
98
+ catch {
70
99
  fs.rmSync(lock, { recursive: true, force: true });
71
100
  return undefined;
72
101
  }
@@ -150,7 +179,7 @@ function options(args, valueFlags) {
150
179
  }
151
180
  return values;
152
181
  }
153
- function runToolCounter(root, action, sessionArg, env, now) {
182
+ function runToolCounter(root, action, sessionArg, env, now, writers) {
154
183
  const rawKey = sessionArg || env.HQ_JOURNAL_SESSION || env.HQ_HOOK_SESSION_ID || "";
155
184
  const key = counterKey(rawKey);
156
185
  const dir = journalDir(root, now().toISOString().slice(0, 10));
@@ -160,31 +189,60 @@ function runToolCounter(root, action, sessionArg, env, now) {
160
189
  catch { /* legacy mkdir failure is ignored */ }
161
190
  const counter = path.join(dir, `.tool-count-${key}`);
162
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
+ };
163
199
  if (action === "increment") {
164
200
  withCounterLock(lock, () => {
165
201
  try {
166
202
  fs.writeFileSync(counter, `${readCounter(counter) + 1}\n`);
167
203
  }
168
- catch { /* fail soft */ }
204
+ catch (error) {
205
+ reportDirectoryWriteFailure(error, 312);
206
+ }
169
207
  });
170
208
  }
171
209
  else if (action === "read") {
172
- process.stdout.write(`${readCounter(counter)}\n`);
210
+ writers.stdout(`${readCounter(counter)}\n`);
173
211
  }
174
212
  else if (action === "reset") {
175
213
  withCounterLock(lock, () => {
176
214
  try {
177
215
  fs.writeFileSync(counter, "0\n");
178
216
  }
179
- catch { /* fail soft */ }
217
+ catch (error) {
218
+ reportDirectoryWriteFailure(error, 320);
219
+ }
180
220
  });
181
221
  }
182
222
  else {
183
- warning("tool-counter: subcommand required (increment|read|reset)");
223
+ writers.stderr("session-journal: tool-counter: subcommand required (increment|read|reset)\n");
184
224
  }
185
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
+ }
186
237
  /** Native implementation of the fail-soft session journal helper. */
187
- 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`); };
188
246
  const runtime = resolveSessionHelperRuntime(dependencies);
189
247
  const today = () => runtime.now().toISOString().slice(0, 10);
190
248
  const nowIso = () => runtime.now().toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -211,6 +269,7 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
211
269
  return 0;
212
270
  }
213
271
  const slug = slugify(title) || "entry";
272
+ const yamlSlug = slug.includes("\n") ? yamlDoubleQuoted(slug) : slug;
214
273
  const body = bodyFile && hasFile(bodyFile)
215
274
  ? fs.readFileSync(bodyFile, "utf8")
216
275
  : "## Goal\n\n## Findings\n\n## Decisions\n\n## Next\n";
@@ -226,7 +285,7 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
226
285
  for (let attempt = 0; attempt < 10_000; attempt += 1) {
227
286
  seq = nextSequence(dir);
228
287
  output = path.join(dir, `${seq}-${slug}.md`);
229
- const lines = ["---\n", `ts: ${nowIso()}\n`, `title: ${title}\n`, `slug: ${slug}\n`];
288
+ const lines = ["---\n", `ts: ${nowIso()}\n`, `title: ${yamlDoubleQuoted(title)}\n`, `slug: ${yamlSlug}\n`];
230
289
  if (files) {
231
290
  lines.push("files:\n");
232
291
  for (const file of files.split(","))
@@ -267,8 +326,8 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
267
326
  finally {
268
327
  releaseJournalWriteLock(writeLock);
269
328
  }
270
- runToolCounter(root, "reset", counterKey(session || runtime.env.HQ_JOURNAL_SESSION || runtime.env.HQ_HOOK_SESSION_ID || ""), runtime.env, runtime.now);
271
- 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`);
272
331
  return 0;
273
332
  }
274
333
  if (command === "list" || command === "read" || command === "index-path" || command === "dir-path") {
@@ -277,19 +336,19 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
277
336
  const date = options(afterSequence, ["--date"]).get("--date") || today();
278
337
  const dir = journalDir(root, date);
279
338
  if (command === "dir-path") {
280
- process.stdout.write(dir);
339
+ writers.stdout(dir);
281
340
  return 0;
282
341
  }
283
342
  if (command === "index-path") {
284
- process.stdout.write(`${dir}/INDEX.md\n`);
343
+ writers.stdout(`${dir}/INDEX.md\n`);
285
344
  return 0;
286
345
  }
287
346
  if (command === "list") {
288
347
  const index = path.join(dir, "INDEX.md");
289
348
  if (hasFile(index))
290
- process.stdout.write(fs.readFileSync(index));
349
+ writers.stdout(fs.readFileSync(index));
291
350
  else
292
- process.stdout.write(`(no journal for ${date})\n`);
351
+ writers.stdout(`(no journal for ${date})\n`);
293
352
  return 0;
294
353
  }
295
354
  if (!sequence) {
@@ -310,7 +369,7 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
310
369
  }
311
370
  })();
312
371
  if (match)
313
- process.stdout.write(fs.readFileSync(path.join(dir, match)));
372
+ writers.stdout(fs.readFileSync(path.join(dir, match)));
314
373
  else
315
374
  warning(`read: no entry ${normalized} for ${date}`);
316
375
  return 0;
@@ -318,10 +377,10 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
318
377
  if (command === "tool-counter") {
319
378
  const action = rest[0] ?? "";
320
379
  const session = options(rest.slice(1), ["--session"]).get("--session") ?? "";
321
- runToolCounter(root, action, session, runtime.env, runtime.now);
380
+ runToolCounter(root, action, session, runtime.env, runtime.now, writers);
322
381
  return 0;
323
382
  }
324
- process.stderr.write(USAGE);
383
+ writers.stderr(USAGE);
325
384
  return 1;
326
385
  }
327
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",