@indigoai-us/hq-cli 5.342.13 → 5.342.14

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,10 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.342.14] — 2026-10-04
6
+ - Fixed native HQ subprocess launching and job-lock command resolution on Windows.
7
+ - Fixed Windows delegate/job shell parity while preserving embedded carriage returns and POSIX command/path bytes.
8
+
5
9
  ## [5.342.13] — 2026-10-04
6
10
  - Run independent core migration differential cases with bounded parallel child processes while preserving per-case reports and failures.
7
11
 
@@ -35,6 +35,15 @@ die() { echo "hq-delegate-grant: $*" >&2; exit 1; }
35
35
 
36
36
  command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
37
37
 
38
+ # jq.exe on Windows emits CRLF data; remove only its line-ending CR bytes.
39
+ case "${OSTYPE:-}" in
40
+ msys*)
41
+ jq() {
42
+ command jq "$@" | command sed -b 's/\r$//'
43
+ }
44
+ ;;
45
+ esac
46
+
38
47
  MANIFEST="" YES=0
39
48
  while [ $# -gt 0 ]; do
40
49
  case "$1" in
@@ -40,6 +40,15 @@ die() { echo "hq-delegate-send: $*" >&2; exit 1; }
40
40
 
41
41
  command -v jq >/dev/null 2>&1 || die "jq is required but not installed"
42
42
 
43
+ # jq.exe on Windows emits CRLF data; remove only its line-ending CR bytes.
44
+ case "${OSTYPE:-}" in
45
+ msys*)
46
+ jq() {
47
+ command jq "$@" | command sed -b 's/\r$//'
48
+ }
49
+ ;;
50
+ esac
51
+
43
52
  MANIFEST="" SEND=0 HEADLINE="" NOTE=""
44
53
  while [ $# -gt 0 ]; do
45
54
  case "$1" in
@@ -120,6 +120,15 @@ done
120
120
  command -v yq >/dev/null 2>&1 || die "yq is required (mikefarah/yq)"
121
121
  command -v jq >/dev/null 2>&1 || die "jq is required"
122
122
 
123
+ # jq.exe on Windows emits CRLF data; remove only its line-ending CR bytes.
124
+ case "${OSTYPE:-}" in
125
+ msys*)
126
+ jq() {
127
+ command jq "$@" | command sed -b 's/\r$//'
128
+ }
129
+ ;;
130
+ esac
131
+
123
132
  iso_now() {
124
133
  if [ -n "${HQ_JOB_PROBE_NOW:-}" ]; then
125
134
  printf '%s' "$HQ_JOB_PROBE_NOW"
@@ -40,6 +40,15 @@ command -v jq >/dev/null 2>&1 || {
40
40
  exit 2
41
41
  }
42
42
 
43
+ # jq.exe on Windows emits CRLF data; remove only its line-ending CR bytes.
44
+ case "${OSTYPE:-}" in
45
+ msys*)
46
+ jq() {
47
+ command jq "$@" | command sed -b 's/\r$//'
48
+ }
49
+ ;;
50
+ esac
51
+
43
52
  [ "$#" -ge 1 ] || die_usage
44
53
 
45
54
  collect_files() {
@@ -4,6 +4,7 @@ import * as fs from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
7
+ import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
7
8
  import { matchingSecretPatterns } from "../lib/core-utils/secret-patterns.js";
8
9
  import { describeCoreCommand } from "./core-command-catalog.js";
9
10
  import { commandExists, isRecord, jsonAt, jqFallback, jqRawPath, jqRawPaths, moveFile, readJsonFile, subprocess, writeJsonAtomically, } from "./core-delegate.js";
@@ -171,12 +172,20 @@ function parseFlags(args, allowed) {
171
172
  }
172
173
  function usage(name) { return USAGE[name] ?? ""; }
173
174
  function usageFailure(name) { err(usage(name)); return 1; }
175
+ function bashPath(value) {
176
+ if (process.platform !== "win32")
177
+ return value;
178
+ const normalized = value.replaceAll("\\", "/");
179
+ const drivePath = /^([A-Za-z]):\/(.*)$/u.exec(normalized);
180
+ return drivePath ? `/${drivePath[1].toLowerCase()}/${drivePath[2]}` : normalized;
181
+ }
174
182
  function runShellFallback(name, args, context) {
183
+ const root = context.root ?? context.cwd;
175
184
  const result = runBundledScript({
176
185
  asset: ASSETS[name],
177
186
  args,
178
187
  cwd: context.cwd,
179
- hqRoot: context.root ?? context.cwd,
188
+ hqRoot: bashPath(root),
180
189
  captureOutput: true,
181
190
  });
182
191
  if (result.stdout?.length)
@@ -392,16 +401,16 @@ function runGrant(args, context) {
392
401
  if (!manifestPath)
393
402
  return fail("grant", "--manifest is required");
394
403
  if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile())
395
- return fail("grant", `manifest not found: ${manifestPath}`);
404
+ return fail("grant", `manifest not found: ${shellDisplayPath(manifestPath)}`);
396
405
  let manifest;
397
406
  try {
398
407
  manifest = readJsonFile(manifestPath);
399
408
  }
400
409
  catch {
401
- return fail("grant", `manifest is not valid JSON: ${manifestPath}`);
410
+ return fail("grant", `manifest is not valid JSON: ${shellDisplayPath(manifestPath)}`);
402
411
  }
403
412
  if (manifest === null || manifest === false)
404
- return fail("grant", `manifest is not valid JSON: ${manifestPath}`);
413
+ return fail("grant", `manifest is not valid JSON: ${shellDisplayPath(manifestPath)}`);
405
414
  if (!isRecord(manifest))
406
415
  return fail("grant", "manifest has no company");
407
416
  const company = jqRawPath(manifest, "company");
@@ -778,13 +787,13 @@ function runSend(args, context) {
778
787
  if (!manifestPath)
779
788
  return fail("send", "--manifest is required");
780
789
  if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile())
781
- return fail("send", `manifest not found: ${manifestPath}`);
790
+ return fail("send", `manifest not found: ${shellDisplayPath(manifestPath)}`);
782
791
  const manifest = jsonFile(manifestPath);
783
792
  if (!manifest)
784
- return fail("send", `manifest is not valid JSON: ${manifestPath}`);
793
+ return fail("send", `manifest is not valid JSON: ${shellDisplayPath(manifestPath)}`);
785
794
  if (send) {
786
795
  const publishedPath = path.join(context.root ?? context.cwd, jqRawPath(manifest, "publication.manifestPath"));
787
- if (fs.existsSync(publishedPath) && fs.statSync(publishedPath).isFile() && malformedJsonFile(publishedPath)) {
796
+ if (malformedJsonFile(publishedPath)) {
788
797
  return runShellFallback("send", args, context);
789
798
  }
790
799
  }
@@ -894,7 +903,7 @@ function runSend(args, context) {
894
903
  }
895
904
  moveFile(stagedPrompt, prompt);
896
905
  fs.rmSync(stage, { recursive: true, force: true });
897
- out(`hq-delegate-send: pickup prompt written: ${prompt}\n`);
906
+ out(`hq-delegate-send: pickup prompt written: ${shellDisplayPath(prompt)}\n`);
898
907
  if (!send)
899
908
  return 0;
900
909
  if (jqRawPath(manifest, "status") !== "verified")
@@ -1035,12 +1044,12 @@ function runVerify(args, context) {
1035
1044
  if (!manifestPath)
1036
1045
  return fail("verify", "--manifest is required (or --dry-run with --company/--project/--to)");
1037
1046
  if (!fs.existsSync(manifestPath) || !fs.statSync(manifestPath).isFile())
1038
- return fail("verify", `manifest not found: ${manifestPath}`);
1047
+ return fail("verify", `manifest not found: ${shellDisplayPath(manifestPath)}`);
1039
1048
  if (malformedJsonFile(manifestPath))
1040
1049
  return runShellFallback("verify", args, context);
1041
1050
  const manifest = jsonFile(manifestPath);
1042
1051
  if (!manifest)
1043
- return fail("verify", `jq: error: ${manifestPath}: Invalid JSON`);
1052
+ return fail("verify", `jq: error: ${shellDisplayPath(manifestPath)}: Invalid JSON`);
1044
1053
  const lock = `${manifestPath}.send-lock`;
1045
1054
  try {
1046
1055
  fs.mkdirSync(lock);
@@ -1,5 +1,6 @@
1
1
  /** Native implementations of the five `hq core delegate` shell companions. */
2
2
  import { Command } from "commander";
3
+ import { type SpawnPlan } from "../utils/windows-spawn.js";
3
4
  export declare const CORE_NATIVE_DELEGATE_FLAG = "core-native-delegate";
4
5
  export type DelegateContext = {
5
6
  cwd: string;
@@ -18,6 +19,7 @@ export type CommandExistsOptions = {
18
19
  existsSync?: (candidate: string) => boolean;
19
20
  };
20
21
  export declare function commandExists(name: string, cwd?: string, options?: CommandExistsOptions): boolean;
22
+ export declare function delegateSubprocessSpawnPlan(command: string, args: readonly string[], env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): SpawnPlan;
21
23
  export declare function subprocess(command: string, args: string[], cwd: string, options?: {
22
24
  input?: Buffer | string;
23
25
  stdinFd?: number;
@@ -4,7 +4,9 @@ import * as fs from "node:fs";
4
4
  import * as os from "node:os";
5
5
  import * as path from "node:path";
6
6
  import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
7
+ import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
7
8
  import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
9
+ import { buildCmdExeSpawnPlan, isWindowsBatchFile } from "../utils/windows-spawn.js";
8
10
  import { describeCoreCommand } from "./core-command-catalog.js";
9
11
  import { resolveLiveRoot } from "../utils/hq-roots.js";
10
12
  import { runBundledScript } from "../utils/run-bundled-script.js";
@@ -86,13 +88,23 @@ function commandPath(name, cwd = process.cwd(), options = {}) {
86
88
  export function commandExists(name, cwd = process.cwd(), options = {}) {
87
89
  return commandPath(name, cwd, options) !== undefined;
88
90
  }
91
+ export function delegateSubprocessSpawnPlan(command, args, env = process.env, platform = process.platform) {
92
+ return isWindowsBatchFile(command, platform)
93
+ ? buildCmdExeSpawnPlan(command, args, env, platform)
94
+ : { cmd: command, args: [...args], shell: false };
95
+ }
89
96
  export function subprocess(command, args, cwd, options = {}) {
90
- const result = spawnSync(command, args, {
97
+ const childEnv = { ...process.env, ...options.env };
98
+ const resolvedCommand = process.platform === "win32" ? commandPath(command, cwd, { env: childEnv }) : undefined;
99
+ const spawn = resolvedCommand ? delegateSubprocessSpawnPlan(resolvedCommand, args, childEnv) : { cmd: command, args, shell: false };
100
+ const result = spawnSync(spawn.cmd, spawn.args, {
91
101
  cwd,
92
- env: { ...process.env, ...options.env },
102
+ env: childEnv,
93
103
  input: options.input,
94
104
  maxBuffer: options.maxBuffer,
95
105
  encoding: "buffer",
106
+ shell: spawn.shell,
107
+ ...(spawn.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
96
108
  stdio: [options.input === undefined ? options.stdinFd ?? "inherit" : "pipe", options.stdout === "inherit" ? "inherit" : options.stdout === "ignore" ? "ignore" : "pipe", options.stderr === "pipe" ? "pipe" : options.stderr ?? "inherit"],
97
109
  });
98
110
  if (result.error) {
@@ -563,11 +575,11 @@ function existingManifest(name, args, context) {
563
575
  }
564
576
  const file = path.resolve(context.cwd, manifestArg);
565
577
  if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
566
- writeErr(`${prefix} manifest not found: ${manifestArg}\n`);
578
+ writeErr(`${prefix} manifest not found: ${shellDisplayPath(manifestArg)}\n`);
567
579
  return { code: 1, parsed };
568
580
  }
569
581
  if (!jqValid(file)) {
570
- writeErr(`${prefix} manifest is not valid JSON: ${manifestArg}\n`);
582
+ writeErr(`${prefix} manifest is not valid JSON: ${shellDisplayPath(manifestArg)}\n`);
571
583
  return { code: 1, parsed };
572
584
  }
573
585
  return { file, parsed, document: readJsonFile(file) };
@@ -1,5 +1,7 @@
1
1
  import type { Command } from "commander";
2
2
  export declare const JOB_PROBE_ASSET = "core/scripts/hq-job-probe.sh";
3
+ export declare function displayJobFile(value: string, platform?: NodeJS.Platform): string;
4
+ export declare function displayShellPath(value: string, platform?: NodeJS.Platform): string;
3
5
  export declare const JOB_PROBE_USAGE: string;
4
6
  export type Checks = {
5
7
  runtime: string;
@@ -30,7 +32,7 @@ export type ProbeDeps = {
30
32
  isDirectory: (file: string) => boolean;
31
33
  realpath: (file: string) => string | undefined;
32
34
  readText: (file: string) => string;
33
- writeState: (file: string, body: string) => void;
35
+ writeState: (file: string, body: string, mode: number) => void;
34
36
  writeStderr: (line: string) => void;
35
37
  listSecrets: (company?: string) => Promise<{
36
38
  code: number;
@@ -39,6 +41,7 @@ export type ProbeDeps = {
39
41
  commandExists: (name: string) => boolean;
40
42
  fetcher: (url: string, init: RequestInit) => Promise<Response>;
41
43
  };
44
+ export declare function hasProbeCommand(name: string, env: NodeJS.ProcessEnv, platform?: NodeJS.Platform, existsSync?: (candidate: string) => boolean): boolean;
42
45
  export declare function defaultProbeDeps(root: string, env?: Record<string, string | undefined>): ProbeDeps;
43
46
  export declare function runJobProbe(argv: string[], injected?: ProbeDeps): Promise<ProbeResult>;
44
47
  export declare function registerCoreJobProbeCommand(job: Command): void;
@@ -8,7 +8,21 @@ import { resolveLiveRoot } from "../utils/hq-roots.js";
8
8
  import { runBundledScript } from "../utils/run-bundled-script.js";
9
9
  import { describeCoreCommand } from "./core-command-catalog.js";
10
10
  import { coreNativeJobsEnabled } from "./core-native-jobs-flag.js";
11
+ import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
12
+ import { planResolvedCommandSpawn } from "../utils/windows-spawn.js";
11
13
  export const JOB_PROBE_ASSET = "core/scripts/hq-job-probe.sh";
14
+ export function displayJobFile(value, platform = process.platform) {
15
+ if (platform !== "win32")
16
+ return value;
17
+ return path.win32.resolve(value).replaceAll("\\", "/");
18
+ }
19
+ export function displayShellPath(value, platform = process.platform) {
20
+ if (platform !== "win32")
21
+ return value;
22
+ const normalized = value.replaceAll("\\", "/");
23
+ const drivePath = /^([A-Za-z]):\/(.*)$/u.exec(normalized);
24
+ return drivePath ? `/${drivePath[1].toLowerCase()}/${drivePath[2]}` : normalized;
25
+ }
12
26
  export const JOB_PROBE_USAGE = [
13
27
  "hq-job-probe.sh — Outpost requirements probe → hq-pro jobs status (US-008).", "",
14
28
  "On the Outpost, deterministically checks a job's requirements checklist and",
@@ -79,8 +93,8 @@ const realpath = (f) => { try {
79
93
  catch {
80
94
  return undefined;
81
95
  } };
82
- function writeState(file, body) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${body}\n`, { mode: 0o600 }); try {
83
- fs.chmodSync(file, 0o600);
96
+ function writeState(file, body, mode) { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, `${body}\n`, { mode }); try {
97
+ fs.chmodSync(file, mode);
84
98
  }
85
99
  catch { /* legacy chmod is best-effort */ } }
86
100
  function hasCommand(name, env) {
@@ -97,10 +111,29 @@ function hasCommand(name, env) {
97
111
  }
98
112
  return false;
99
113
  }
114
+ export function hasProbeCommand(name, env, platform = process.platform, existsSync = (candidate) => { try {
115
+ return fs.statSync(candidate).isFile();
116
+ }
117
+ catch {
118
+ return false;
119
+ } }) {
120
+ return platform === "win32"
121
+ ? resolveWindowsExecutable(name, env, existsSync, process.cwd()) !== null
122
+ : hasCommand(name, env);
123
+ }
100
124
  function listSecrets(company) {
101
125
  // Keep the `hq secrets list` subprocess: no in-process API produces the same names-only table.
102
126
  const args = company ? ["secrets", "list", "--company", company] : ["secrets", "list", "--personal"];
103
- const r = spawnSync("hq", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
127
+ const executable = process.platform === "win32"
128
+ ? resolveWindowsExecutable("hq", process.env, (candidate) => { try {
129
+ return fs.statSync(candidate).isFile();
130
+ }
131
+ catch {
132
+ return false;
133
+ } }, process.cwd()) ?? "hq"
134
+ : "hq";
135
+ const plan = planResolvedCommandSpawn(executable, args, process.env);
136
+ const r = spawnSync(plan.cmd, plan.args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], shell: plan.shell, ...(plan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}) });
104
137
  return Promise.resolve({ code: r.status ?? (r.error ? 127 : 1), stdout: r.stdout ?? "" });
105
138
  }
106
139
  export function defaultProbeDeps(root, env = process.env) {
@@ -111,7 +144,7 @@ export function defaultProbeDeps(root, env = process.env) {
111
144
  statSize, isFile, isDirectory, realpath, readText: (f) => fs.readFileSync(f, "utf8"), writeState,
112
145
  writeStderr: (line) => process.stderr.write(line),
113
146
  listSecrets: stubs ? async () => { throw new Error("HQ_JOB_TEST_STUB blocked real hq secrets list"); } : listSecrets,
114
- commandExists: (name) => hasCommand(name, env),
147
+ commandExists: (name) => hasProbeCommand(name, env),
115
148
  fetcher: stubs ? async () => { throw new Error("HQ_JOB_TEST_STUB blocked real network fetch"); } : (url, init) => fetch(url, init) };
116
149
  }
117
150
  const isoNow = (d) => d.env.HQ_JOB_PROBE_NOW || d.now().toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -148,9 +181,21 @@ function legacyYamlParseDiagnostics(file, d) {
148
181
  // The shell evaluates each field with yq before checking the now-empty id;
149
182
  // preserve the six parser diagnostics it emits on malformed input.
150
183
  const expressions = [".id // \"\"", ".name // \"\"", ".runtime // \"\"", ".requirements.runtime // \"\"", ".requirements.company // \"\"", ".requirements.cwd // .cwd // \"\""];
151
- return expressions.map((expression) => spawnSync("yq", ["-r", expression, file], {
152
- cwd: d.root, env: d.env, encoding: "utf8",
153
- }).stderr ?? "").join("");
184
+ return expressions.map((expression) => {
185
+ const executable = process.platform === "win32"
186
+ ? resolveWindowsExecutable("yq", d.env, (candidate) => { try {
187
+ return fs.statSync(candidate).isFile();
188
+ }
189
+ catch {
190
+ return false;
191
+ } }, d.root) ?? "yq"
192
+ : "yq";
193
+ const plan = planResolvedCommandSpawn(executable, ["-r", expression, file], d.env);
194
+ return spawnSync(plan.cmd, plan.args, {
195
+ cwd: d.root, env: d.env, encoding: "utf8", shell: plan.shell,
196
+ ...(plan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
197
+ }).stderr ?? "";
198
+ }).join("");
154
199
  }
155
200
  function findById(id, d) {
156
201
  let found;
@@ -240,8 +285,8 @@ function auth(runtime, d) {
240
285
  const file = runtime === "claude" ? d.env.HQ_JOB_PROBE_CLAUDE_CRED || path.join(d.home, ".claude/.credentials.json") : d.env.HQ_JOB_PROBE_CODEX_AUTH || path.join(d.home, ".codex/auth.json");
241
286
  return (d.statSize(file) ?? 0) > 0 ? "ok" : "missing";
242
287
  }
243
- function writeAttempt(d, id, data) { d.writeState(path.join(d.env.HQ_JOB_PROBE_STATE_DIR || path.join(d.home, ".hq/jobs/probes"), id, "last-attempt.json"), compact(data)); }
244
- function writeCache(d, id, readiness, source) { d.writeState(path.join(d.env.HQ_JOB_STATUS_CACHE_DIR || path.join(d.home, ".hq/jobs/status-cache"), `${id}.json`), compact({ job_id: id, readiness, updated_at: isoNow(d), source })); }
288
+ function writeAttempt(d, id, data) { d.writeState(path.join(d.env.HQ_JOB_PROBE_STATE_DIR || path.join(d.home, ".hq/jobs/probes"), id, "last-attempt.json"), compact(data), 0o600); }
289
+ function writeCache(d, id, readiness, source) { d.writeState(path.join(d.env.HQ_JOB_STATUS_CACHE_DIR || path.join(d.home, ".hq/jobs/status-cache"), `${id}.json`), compact({ job_id: id, readiness, updated_at: isoNow(d), source }), 0o600); }
245
290
  function resolveCwd(root, cwd, d) {
246
291
  const canon = d.realpath(root) ?? path.resolve(root), candidate = path.resolve(root, cwd), file = d.realpath(candidate) ?? candidate;
247
292
  return { file, allowed: file === canon || file.startsWith(`${canon}${path.sep}`) };
@@ -293,13 +338,13 @@ async function probeFile(file, dry, d) {
293
338
  catch (e) {
294
339
  const rawStderr = legacyYamlParseDiagnostics(file, d);
295
340
  if (rawStderr)
296
- throw new Fatal(`job missing id: ${file}`, 2, "hq-job-probe", rawStderr);
341
+ throw new Fatal(`job missing id: ${displayShellPath(file)}`, 2, "hq-job-probe", rawStderr);
297
342
  throw new Fatal(`invalid job YAML: ${file}${e instanceof Error ? `: ${e.message}` : ""}`);
298
343
  }
299
344
  const id = scalar(job.id), name = scalar(job.name), runtime = scalar(nested(job, "requirements", "runtime")) || scalar(job.runtime);
300
345
  const company = scalar(nested(job, "requirements", "company")), cwd = scalar(nested(job, "requirements", "cwd") ?? job.cwd);
301
346
  if (!id)
302
- throw new Fatal(`job missing id: ${file}`);
347
+ throw new Fatal(`job missing id: ${displayShellPath(file)}`);
303
348
  if (!runtime)
304
349
  throw new Fatal(`job missing runtime: ${file}`);
305
350
  const at = isoNow(d), actions = [], authState = auth(runtime, d);
@@ -411,7 +456,7 @@ async function probeFile(file, dry, d) {
411
456
  }
412
457
  }
413
458
  }
414
- return { code: ingestFailed ? 3 : blocked ? 1 : 0, value: { job_id: id, name, job_file: file, readiness: finalReadiness, probed_at: at, ingest, http, dry_run: dry, checks, next_actions: finalActions } };
459
+ return { code: ingestFailed ? 3 : blocked ? 1 : 0, value: { job_id: id, name, job_file: displayJobFile(file), readiness: finalReadiness, probed_at: at, ingest, http, dry_run: dry, checks, next_actions: finalActions } };
415
460
  }
416
461
  function parseArgs(argv) {
417
462
  const a = [...argv];
@@ -470,7 +515,7 @@ export async function runJobProbe(argv, injected) {
470
515
  return { code: 2, stdout: "", stderr: "hq-job-probe: --all does not take a job argument\n" };
471
516
  const files = allFiles(root);
472
517
  if (!files.length)
473
- return { code: 2, stdout: "", stderr: `hq-job-probe: no job YAML found under personal/jobs or companies/*/jobs in ${root}\n` };
518
+ return { code: 2, stdout: "", stderr: `hq-job-probe: no job YAML found under personal/jobs or companies/*/jobs in ${displayShellPath(root)}\n` };
474
519
  let code = 0;
475
520
  try {
476
521
  for (const f of files) {
@@ -500,7 +545,10 @@ export async function runJobProbe(argv, injected) {
500
545
  // then emits the ordinary not-found error after the process substitution
501
546
  // closes. Preserve both stderr lines for byte parity.
502
547
  if (x instanceof Fatal && x.message.startsWith("duplicate job id '")) {
503
- return { code: x.code, stdout: "", stderr: `${x.prefix}: ${x.message}\n${x.prefix}: job id not found in registry: ${opts.target}\n` };
548
+ const shellMessage = process.platform === "win32"
549
+ ? x.message.replace(/([A-Za-z]):[\\/]/gu, (_match, drive) => "/" + drive.toLowerCase() + "/").replaceAll("\\", "/")
550
+ : x.message;
551
+ return { code: x.code, stdout: "", stderr: `${x.prefix}: ${shellMessage}\n${x.prefix}: job id not found in registry: ${opts.target}\n` };
504
552
  }
505
553
  return failure(x);
506
554
  }
@@ -2,6 +2,8 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
+ import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
6
+ import { planResolvedCommandSpawn } from "../utils/windows-spawn.js";
5
7
  import { fileURLToPath } from "node:url";
6
8
  import { JSON_SCHEMA, load } from "js-yaml";
7
9
  import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
@@ -256,14 +258,24 @@ export async function spawnProcess(request) {
256
258
  }
257
259
  /** Holds the same kernel flock(2) lock used by the shell companion. */
258
260
  export async function acquireFlock(file, env = process.env) {
259
- const binary = env.HQ_JOB_FLOCK_BIN || pathExecutable("flock", env);
261
+ const binary = env.HQ_JOB_FLOCK_BIN || (process.platform === "win32"
262
+ ? resolveWindowsExecutable("flock", env, (candidate) => { try {
263
+ return fs.statSync(candidate).isFile();
264
+ }
265
+ catch {
266
+ return false;
267
+ } }, process.cwd())
268
+ : pathExecutable("flock", env));
260
269
  if (!binary)
261
270
  throw new Error("Outpost/Linux requires util-linux flock(1) for serialized job runs");
262
271
  fs.mkdirSync(path.dirname(file), { recursive: true });
263
272
  return await new Promise((resolve, reject) => {
264
- const child = spawn(binary, ["-x", file, process.execPath, "-e", LOCK_HOLDER], {
273
+ const spawnPlan = planResolvedCommandSpawn(binary, ["-x", file, process.execPath, "-e", LOCK_HOLDER], env);
274
+ const child = spawn(spawnPlan.cmd, spawnPlan.args, {
265
275
  env,
266
276
  stdio: ["pipe", "pipe", "pipe"],
277
+ shell: spawnPlan.shell,
278
+ ...(spawnPlan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
267
279
  });
268
280
  let stdout = "";
269
281
  let stderr = "";
@@ -4,6 +4,7 @@ import { spawnSync } from "node:child_process";
4
4
  import { JSON_SCHEMA, load, loadAll } from "js-yaml";
5
5
  import { matchingSecretPatterns } from "../lib/core-utils/secret-patterns.js";
6
6
  import { serializeJqJson } from "../lib/core-utils/jq-json-serializer.js";
7
+ import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
7
8
  import { resolveLiveRoot } from "../utils/hq-roots.js";
8
9
  import { runBundledScript } from "../utils/run-bundled-script.js";
9
10
  import { describeCoreCommand } from "./core-command-catalog.js";
@@ -972,7 +973,7 @@ function systemdQuote(value) {
972
973
  }
973
974
  function serviceUnit(root, runner, id) {
974
975
  const pathValue = systemdQuote(`PATH=${servicePathValue()}`);
975
- return `# Generated by outpost-jobs-reconcile.sh — disposable derived state. Do not edit.\n[Unit]\nDescription=HQ Outpost scheduled job: ${id}\nDocumentation=file://${root}/core/knowledge/public/hq-core/outpost-jobs-spec.md\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=HQ_ROOT=${root}\nEnvironment=${pathValue}\nExecStart=${runner} --hq-root ${root} --job-id ${id}\nNice=10\n\n[Install]\nWantedBy=default.target\n`;
976
+ return `# Generated by outpost-jobs-reconcile.sh — disposable derived state. Do not edit.\n[Unit]\nDescription=HQ Outpost scheduled job: ${id}\nDocumentation=file://${root}/core/knowledge/public/hq-core/outpost-jobs-spec.md\nAfter=network-online.target\nWants=network-online.target\n\n[Service]\nType=oneshot\nEnvironment=HQ_ROOT=${root}\nEnvironment=${pathValue}\nExecStart=${shellDisplayPath(runner)} --hq-root ${root} --job-id ${id}\nNice=10\n\n[Install]\nWantedBy=default.target\n`;
976
977
  }
977
978
  function timerUnit(id, onCalendar, randomize, root) {
978
979
  return `# Generated by outpost-jobs-reconcile.sh — disposable derived state. Do not edit.\n[Unit]\nDescription=HQ Outpost scheduled job timer: ${id}\nDocumentation=file://${root}/core/knowledge/public/hq-core/outpost-jobs-spec.md\n\n[Timer]\nOnCalendar=${onCalendar}\nPersistent=true\nRandomizedDelaySec=${randomize}\nUnit=hq-job-${id}.service\n\n[Install]\nWantedBy=timers.target\n`;
@@ -3,8 +3,10 @@ import { createHash } from "node:crypto";
3
3
  import { spawnSync } from "node:child_process";
4
4
  import * as fs from "node:fs";
5
5
  import * as path from "node:path";
6
+ import { resolveWindowsExecutable } from "../lib/bot/runtime/windows.js";
6
7
  import { runBundledScript } from "../utils/run-bundled-script.js";
7
8
  import { resolveBundledAsset } from "../utils/hq-roots.js";
9
+ import { planResolvedCommandSpawn } from "../utils/windows-spawn.js";
8
10
  import { describeCoreCommand } from "./core-command-catalog.js";
9
11
  import { runKnowledgePrefs } from "./core-knowledge-prefs.js";
10
12
  const FLAG = "core-native-ontology";
@@ -55,10 +57,23 @@ export function runOntologyNativeFast(args, _stdin, cwd, hqRoot, commandName) {
55
57
  }
56
58
  function runHq(args, output = "capture") {
57
59
  const executable = process.env.HQ_BIN || "hq";
58
- const result = spawnSync(executable, args, {
60
+ const resolvedExecutable = process.platform === "win32" && !path.isAbsolute(executable) && !executable.includes(path.sep)
61
+ ? resolveWindowsExecutable(executable, process.env, (candidate) => {
62
+ try {
63
+ return fs.statSync(candidate).isFile();
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }, process.cwd()) ?? executable
69
+ : executable;
70
+ const plan = planResolvedCommandSpawn(resolvedExecutable, args, process.env);
71
+ const result = spawnSync(plan.cmd, plan.args, {
59
72
  cwd: process.cwd(),
60
73
  env: process.env,
61
74
  encoding: "utf8",
75
+ shell: plan.shell,
76
+ ...(plan.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
62
77
  stdio: output === "ignore" ? ["ignore", "ignore", "ignore"] : ["ignore", "pipe", "ignore"],
63
78
  });
64
79
  return { status: result.status, stdout: typeof result.stdout === "string" ? result.stdout : "" };
@@ -522,7 +537,8 @@ export function gardenCommand(args, defaultRoot) {
522
537
  source_ref: JSON.stringify(fm.source_ref || ""), created_at: String(fm.created_at || isoNow()),
523
538
  }, body, dryRun);
524
539
  report.signals_promoted += 1;
525
- index.push({ signal_id: id, type, audience_key: key, path: path.relative(companyDir, outputFile) });
540
+ const outputPath = path.relative(companyDir, outputFile);
541
+ index.push({ signal_id: id, type, audience_key: key, path: process.platform === "win32" ? outputPath.replaceAll("\\", "/") : outputPath });
526
542
  if (key !== "company") {
527
543
  const audienceFile = path.join(scopeDirectory, "_audience.yaml");
528
544
  if (!fs.existsSync(audienceFile) && !dryRun) {
@@ -56,7 +56,8 @@ function isRegularFile(filePath, fileSystem) {
56
56
  }
57
57
  }
58
58
  function readLink(filePath, fileSystem) {
59
- return fileSystem.readlinkSync(filePath);
59
+ const target = fileSystem.readlinkSync(filePath);
60
+ return process.platform === "win32" ? target.replaceAll("\\", "/") : target;
60
61
  }
61
62
  function sameBytes(leftPath, rightPath, fileSystem) {
62
63
  try {
@@ -0,0 +1,3 @@
1
+ /** Render a native path with the path spelling used by the bundled Bash scripts. */
2
+ export declare function shellDisplayPath(value: string): string;
3
+ //# sourceMappingURL=shell-display-path.d.ts.map
@@ -0,0 +1,12 @@
1
+ import * as path from "node:path";
2
+ /** Render a native path with the path spelling used by the bundled Bash scripts. */
3
+ export function shellDisplayPath(value) {
4
+ if (process.platform !== "win32")
5
+ return value;
6
+ const absolute = path.win32.resolve(value);
7
+ const match = /^([A-Za-z]):[\\/](.*)$/.exec(absolute);
8
+ if (!match?.[1] || match[2] === undefined)
9
+ return value;
10
+ return `/${match[1].toLowerCase()}/${match[2].replaceAll("\\", "/")}`;
11
+ }
12
+ //# sourceMappingURL=shell-display-path.js.map
@@ -57,6 +57,8 @@ export type RunBundledScriptOptions = {
57
57
  captureOutput?: boolean;
58
58
  /** Bytes supplied on stdin. With no value, stdin is inherited as before. */
59
59
  stdin?: string | Buffer;
60
+ /** Optional child-process deadline for callers that need bounded script execution. */
61
+ timeoutMs?: number;
60
62
  };
61
63
  export type RunBundledScriptResult = {
62
64
  /** The child's exit code, or 128+signal if it was terminated by one. */
@@ -189,6 +189,7 @@ export function runBundledScript(opts) {
189
189
  cwd: opts.cwd,
190
190
  env,
191
191
  stdio,
192
+ ...(opts.timeoutMs === undefined ? {} : { timeout: opts.timeoutMs }),
192
193
  ...(opts.captureOutput ? { maxBuffer: Number.MAX_SAFE_INTEGER } : {}),
193
194
  });
194
195
  }
@@ -47,4 +47,6 @@ export declare function isWindowsBatchFile(command: string, platform?: NodeJS.Pl
47
47
  * passed through untouched.
48
48
  */
49
49
  export declare function planCommandSpawn(command: string, args: readonly string[], platform?: NodeJS.Platform): SpawnPlan;
50
+ /** Spawn a resolved executable, routing only Windows batch shims through the escaped cmd.exe plan. */
51
+ export declare function planResolvedCommandSpawn(command: string, args: readonly string[], env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): SpawnPlan;
50
52
  //# sourceMappingURL=windows-spawn.d.ts.map
@@ -108,4 +108,10 @@ export function planCommandSpawn(command, args, platform = process.platform) {
108
108
  return buildSpawnPlan(command, args, platform);
109
109
  return { cmd: command, args: [...args], shell: false };
110
110
  }
111
+ /** Spawn a resolved executable, routing only Windows batch shims through the escaped cmd.exe plan. */
112
+ export function planResolvedCommandSpawn(command, args, env = process.env, platform = process.platform) {
113
+ if (isWindowsBatchFile(command, platform))
114
+ return buildCmdExeSpawnPlan(command, args, env, platform);
115
+ return { cmd: command, args: [...args], shell: false };
116
+ }
111
117
  //# sourceMappingURL=windows-spawn.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.342.13",
3
+ "version": "5.342.14",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {