@indigoai-us/hq-cli 5.335.1-beta.0 → 5.336.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,8 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## [5.335.1-beta.0] — 2026-10-03
5
+ ## [5.336.0] — 2026-10-03
6
6
  - Client-health refresh failures now retain the bounded Cognito OAuth error class for accurate auth diagnostics.
7
+ - Fix macOS Bash 3.2 service PATH generation when nounset is enabled.
8
+ - Parse policy creation dates at midnight UTC and reject calendar-invalid dates consistently across the shell and native report.
9
+ - Classify missing hidden references by basename-scoped extension siblings, and escape C1 bytes in session journal YAML on every platform.
10
+ - Match BSD awk diagnostics for company resolution and policy frontmatter.
7
11
  - CI: Run main-push Linux ARM checks on the reserved self-hosted runner pool.
8
12
 
9
13
  ## [5.335.0] — 2026-10-03
@@ -413,7 +413,7 @@ unit_service_path() { printf '%s/hq-job-%s.service' "$UNIT_DIR" "$1"; }
413
413
  unit_timer_path() { printf '%s/hq-job-%s.timer' "$UNIT_DIR" "$1"; }
414
414
 
415
415
  normalize_service_path() {
416
- local remaining="$1" entry existing normalized="" has_more seen
416
+ local remaining="$1" entry existing normalized="" has_more seen entry_count=0 index
417
417
  local -a entries=()
418
418
  while :; do
419
419
  if [[ "$remaining" == *:* ]]; then
@@ -428,7 +428,8 @@ normalize_service_path() {
428
428
  case "$entry" in
429
429
  /*)
430
430
  seen=0
431
- for existing in "${entries[@]}"; do
431
+ for ((index = 0; index < entry_count; index++)); do
432
+ existing="${entries[index]}"
432
433
  if [ "$existing" = "$entry" ]; then
433
434
  seen=1
434
435
  break
@@ -436,12 +437,13 @@ normalize_service_path() {
436
437
  done
437
438
  if [ "$seen" -eq 0 ]; then
438
439
  entries+=("$entry")
440
+ entry_count=$((entry_count + 1))
439
441
  fi
440
442
  ;;
441
443
  esac
442
444
  [ "$has_more" -eq 1 ] || break
443
445
  done
444
- if [ "${#entries[@]}" -gt 0 ]; then
446
+ if [ "$entry_count" -gt 0 ]; then
445
447
  local IFS=:
446
448
  normalized="${entries[*]}"
447
449
  fi
@@ -100,7 +100,12 @@ else
100
100
  fi
101
101
 
102
102
  to_epoch() { # YYYY-MM-DD -> epoch (0 on failure)
103
- date -u -d "$1" +%s 2>/dev/null || date -u -j -f %Y-%m-%d "$1" +%s 2>/dev/null || echo 0
103
+ local epoch normalized
104
+ epoch="$(date -u -d "$1 00:00:00" +%s 2>/dev/null || date -u -j -f '%Y-%m-%d %H:%M:%S' "$1 00:00:00" +%s 2>/dev/null)" || { echo 0; return; }
105
+ [ -n "$epoch" ] || { echo 0; return; }
106
+ normalized="$(date -u -d "@$epoch" +%Y-%m-%d 2>/dev/null || date -u -r "$epoch" +%Y-%m-%d 2>/dev/null)" || { echo 0; return; }
107
+ [ "$normalized" = "$1" ] || { echo 0; return; }
108
+ echo "$epoch"
104
109
  }
105
110
 
106
111
  for dir in "${DIRS[@]}"; do
@@ -144,7 +149,18 @@ for dir in "${DIRS[@]}"; do
144
149
  if [ ! -e "$HQ_ROOT/$ref" ]; then
145
150
  # A sibling with the same stem (renamed extension: foo.sh -> foo.mjs) means
146
151
  # the subject still exists and the rule needs a fix, not retirement.
147
- stem="${ref%.*}"
152
+ ref_for_stem="${ref%/}"
153
+ ref_basename="${ref_for_stem##*/}"
154
+ case "$ref_basename" in
155
+ .* )
156
+ case "${ref_basename#.}" in
157
+ *.*) stem="${ref_for_stem%.*}" ;;
158
+ *) stem="$ref_for_stem" ;;
159
+ esac
160
+ ;;
161
+ *.*) stem="${ref_for_stem%.*}" ;;
162
+ *) stem="$ref_for_stem" ;;
163
+ esac
148
164
  if ls "$HQ_ROOT/$stem".* >/dev/null 2>&1; then renamed="${renamed:+$renamed,}$ref"; else stale="${stale:+$stale,}$ref"; fi
149
165
  fi
150
166
  done < <(awk '/^## Rule/{r=1;next} r&&/^## /{r=0} r' "$f" \
@@ -44,15 +44,49 @@ slugify() {
44
44
  }
45
45
 
46
46
  yaml_double_quoted() {
47
- local value="$1" output='"' character next codepoint hex
47
+ local value="$1" output='"' character next pair hex
48
48
  local LC_ALL=C index
49
49
  for ((index = 0; index < ${#value}; index++)); do
50
50
  character="${value:index:1}"
51
51
  if [[ "$character" == $'\xc2' ]] && (( index + 1 < ${#value} )); then
52
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"
53
+ pair="$character$next"
54
+ case "$pair" in
55
+ $'\xc2\x80') hex=0080 ;;
56
+ $'\xc2\x81') hex=0081 ;;
57
+ $'\xc2\x82') hex=0082 ;;
58
+ $'\xc2\x83') hex=0083 ;;
59
+ $'\xc2\x84') hex=0084 ;;
60
+ $'\xc2\x85') hex=0085 ;;
61
+ $'\xc2\x86') hex=0086 ;;
62
+ $'\xc2\x87') hex=0087 ;;
63
+ $'\xc2\x88') hex=0088 ;;
64
+ $'\xc2\x89') hex=0089 ;;
65
+ $'\xc2\x8a') hex=008a ;;
66
+ $'\xc2\x8b') hex=008b ;;
67
+ $'\xc2\x8c') hex=008c ;;
68
+ $'\xc2\x8d') hex=008d ;;
69
+ $'\xc2\x8e') hex=008e ;;
70
+ $'\xc2\x8f') hex=008f ;;
71
+ $'\xc2\x90') hex=0090 ;;
72
+ $'\xc2\x91') hex=0091 ;;
73
+ $'\xc2\x92') hex=0092 ;;
74
+ $'\xc2\x93') hex=0093 ;;
75
+ $'\xc2\x94') hex=0094 ;;
76
+ $'\xc2\x95') hex=0095 ;;
77
+ $'\xc2\x96') hex=0096 ;;
78
+ $'\xc2\x97') hex=0097 ;;
79
+ $'\xc2\x98') hex=0098 ;;
80
+ $'\xc2\x99') hex=0099 ;;
81
+ $'\xc2\x9a') hex=009a ;;
82
+ $'\xc2\x9b') hex=009b ;;
83
+ $'\xc2\x9c') hex=009c ;;
84
+ $'\xc2\x9d') hex=009d ;;
85
+ $'\xc2\x9e') hex=009e ;;
86
+ $'\xc2\x9f') hex=009f ;;
87
+ *) hex= ;;
88
+ esac
89
+ if [[ -n "$hex" ]]; then
56
90
  output+="\\u$hex"
57
91
  index=$((index + 1))
58
92
  continue
@@ -4,5 +4,5 @@ export type FrontmatterResult = {
4
4
  stderr: Buffer;
5
5
  };
6
6
  /** Byte-preserving port of core/scripts/read-policy-frontmatter.sh. */
7
- export declare function runPolicyFrontmatter(args: readonly string[], cwd: string): FrontmatterResult;
7
+ export declare function runPolicyFrontmatter(args: readonly string[], cwd: string, pathValue?: string | undefined, platform?: NodeJS.Platform): FrontmatterResult;
8
8
  //# sourceMappingURL=core-policy-frontmatter.d.ts.map
@@ -1,8 +1,9 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { usesGnuAwk } from '../lib/core-utils/awk-diagnostics.js';
3
4
  const USAGE = Buffer.from('USAGE: read-policy-frontmatter.sh <policy-file>\n');
4
5
  /** Byte-preserving port of core/scripts/read-policy-frontmatter.sh. */
5
- export function runPolicyFrontmatter(args, cwd) {
6
+ export function runPolicyFrontmatter(args, cwd, pathValue = process.env.PATH, platform = process.platform) {
6
7
  const first = args[0];
7
8
  if (first === undefined)
8
9
  return { code: 1, stdout: Buffer.alloc(0), stderr: USAGE };
@@ -18,7 +19,9 @@ export function runPolicyFrontmatter(args, cwd) {
18
19
  return {
19
20
  code: 0,
20
21
  stdout: Buffer.alloc(0),
21
- stderr: Buffer.from(`awk: warning: command line argument \`${first}' is a directory: skipped\n`),
22
+ stderr: Buffer.from(usesGnuAwk(pathValue, platform)
23
+ ? `awk: warning: command line argument \`${first}' is a directory: skipped\n`
24
+ : ''),
22
25
  };
23
26
  }
24
27
  if (code === 'ENOENT' || code === 'ENOTDIR' || code === 'EACCES' || code === 'ELOOP') {
@@ -5,8 +5,8 @@ export type CorePolicyResult = {
5
5
  stdout: Buffer;
6
6
  stderr: Buffer;
7
7
  };
8
- export declare function runPolicyAgeReport(args: readonly string[], root: string, env?: NodeJS.ProcessEnv, nowEpoch?: number): CorePolicyResult;
9
- export declare function runPolicyFrontmatter(args: readonly string[], cwd: string): FrontmatterResult;
8
+ export declare function runPolicyAgeReport(args: readonly string[], root: string, env?: NodeJS.ProcessEnv, nowEpoch?: number, platform?: NodeJS.Platform): CorePolicyResult;
9
+ export declare function runPolicyFrontmatter(args: readonly string[], cwd: string, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): FrontmatterResult;
10
10
  export declare function runPolicyRetire(args: readonly string[], root: string, cwd: string, env?: NodeJS.ProcessEnv, now?: Date): CorePolicyResult;
11
11
  /** Fast-entry adapter; family gating and shell fallback happen before import. */
12
12
  export declare function runPolicyFast(args: readonly string[], _stdin: string, cwd: string, hqRoot: string | undefined, commandName: string): number;
@@ -206,8 +206,11 @@ function extractRulePaths(file) {
206
206
  return [...paths].sort();
207
207
  }
208
208
  function shellReferenceStem(reference) {
209
- const lastDot = reference.lastIndexOf('.');
210
- return lastDot === -1 ? reference : reference.slice(0, lastDot);
209
+ const normalized = reference.replace(/\/$/, '');
210
+ const slash = normalized.lastIndexOf('/');
211
+ const basename = normalized.slice(slash + 1);
212
+ const lastDot = basename.lastIndexOf('.');
213
+ return lastDot <= 0 ? normalized : normalized.slice(0, slash + 1 + lastDot);
211
214
  }
212
215
  function shellLsExtensionGlobHasMatch(root, stem) {
213
216
  const patternPath = path.join(root, `${stem}.*`);
@@ -223,8 +226,8 @@ function shellLsExtensionGlobHasMatch(root, stem) {
223
226
  return false;
224
227
  throw error;
225
228
  }
226
- // Bash 5.2 enables globskipdots by default, so `.*` only matches when the
227
- // directory has an actual hidden entry. readdir omits `.` and `..`.
229
+ // The shell checks extension siblings only within the reference basename's
230
+ // directory, so directory dot-components never count as sibling files.
228
231
  return entries.some((entry) => entry.startsWith(literalPrefix));
229
232
  }
230
233
  function refSummary(file, root) {
@@ -260,12 +263,17 @@ function refSummary(file, root) {
260
263
  return { stale, renamed, judged: judged.length };
261
264
  }
262
265
  function epochFromDate(value) {
263
- if (!value)
266
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
264
267
  return 0;
265
- const parsed = Date.parse(`${value}T00:00:00Z`);
266
- if (!Number.isFinite(parsed) || new Date(parsed).toISOString().slice(0, 10) !== value)
268
+ const year = Number(value.slice(0, 4));
269
+ const month = Number(value.slice(5, 7));
270
+ const day = Number(value.slice(8, 10));
271
+ if (month < 1 || month > 12 || day < 1 || day > 31)
267
272
  return 0;
268
- return Math.floor(parsed / 1000);
273
+ const parsed = new Date(Date.UTC(year, month - 1, day));
274
+ if (parsed.getUTCFullYear() !== year || parsed.getUTCMonth() + 1 !== month || parsed.getUTCDate() !== day)
275
+ return 0;
276
+ return Math.floor(parsed.getTime() / 1000);
269
277
  }
270
278
  function countLedgerRows(directory, withMtime, recursive, awkPath) {
271
279
  const counts = new Map();
@@ -380,7 +388,7 @@ function bashTestGreaterOrEqual(left, right, scriptPath, line) {
380
388
  }
381
389
  return { value: BigInt(left) >= parsed, stderr: '' };
382
390
  }
383
- export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Math.floor(Date.now() / 1000)) {
391
+ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Math.floor(Date.now() / 1000), platform = process.platform) {
384
392
  const parsed = parseAgeOptions(args, resolveBundledAsset(POLICY_AGE_REPORT_ASSET));
385
393
  if (parsed.kind === 'help')
386
394
  return policyResult(0, AGE_HELP);
@@ -440,13 +448,13 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
440
448
  const singleIncident = ['user-correction', 'session-learning', 'back-pressure-failure'].includes(source) && (version || '1') === '1';
441
449
  const classes = [];
442
450
  if (fired === 0) {
443
- const comparison = bashTestGreaterOrEqual(ageDays, parsed.minAge, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 157);
451
+ const comparison = bashTestGreaterOrEqual(ageDays, parsed.minAge, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 173);
444
452
  stderr += comparison.stderr;
445
453
  if (comparison.value && usage.anyFiles)
446
454
  classes.push('never-fired');
447
455
  }
448
456
  if (fired > 0) {
449
- const comparison = bashTestGreaterOrEqual(lastAge, parsed.dormantDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 158);
457
+ const comparison = bashTestGreaterOrEqual(lastAge, parsed.dormantDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 174);
450
458
  stderr += comparison.stderr;
451
459
  if (comparison.value)
452
460
  classes.push('dormant');
@@ -456,7 +464,7 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
456
464
  if (refs.renamed.length > 0)
457
465
  classes.push('renamed-refs');
458
466
  if (singleIncident) {
459
- const comparison = bashTestGreaterOrEqual(ageDays, parsed.incidentDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 162);
467
+ const comparison = bashTestGreaterOrEqual(ageDays, parsed.incidentDays, resolveBundledAsset(POLICY_AGE_REPORT_ASSET), 178);
460
468
  stderr += comparison.stderr;
461
469
  if (comparison.value && fired <= 1)
462
470
  classes.push('single-incident-old');
@@ -496,8 +504,8 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
496
504
  }
497
505
  return policyResult(0, stdout, stderr);
498
506
  }
499
- export function runPolicyFrontmatter(args, cwd) {
500
- return runPolicyFrontmatterImpl(args, cwd);
507
+ export function runPolicyFrontmatter(args, cwd, env = process.env, platform = process.platform) {
508
+ return runPolicyFrontmatterImpl(args, cwd, env.PATH, platform);
501
509
  }
502
510
  function parseRetireOptions(args, scriptPath, env) {
503
511
  const value = {
@@ -47,7 +47,7 @@ function readManifest(filePath, fileSystem, pathValue) {
47
47
  return {
48
48
  code: 0,
49
49
  stdout: Buffer.from('{"company":"","source":"none"}\n'),
50
- stderr: Buffer.from(awkCannotOpenFileDiagnostic(filePath, "Permission denied", 1, process.platform, pathValue)),
50
+ stderr: Buffer.from(awkCannotOpenFileDiagnostic(filePath, "Permission denied", 11, process.platform, pathValue)),
51
51
  };
52
52
  }
53
53
  if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP")
@@ -202,7 +202,7 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
202
202
  fs.writeFileSync(counter, `${readCounter(counter) + 1}\n`);
203
203
  }
204
204
  catch (error) {
205
- reportDirectoryWriteFailure(error, 312);
205
+ reportDirectoryWriteFailure(error, 346);
206
206
  }
207
207
  });
208
208
  }
@@ -215,7 +215,7 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
215
215
  fs.writeFileSync(counter, "0\n");
216
216
  }
217
217
  catch (error) {
218
- reportDirectoryWriteFailure(error, 320);
218
+ reportDirectoryWriteFailure(error, 354);
219
219
  }
220
220
  });
221
221
  }
@@ -1,2 +1,9 @@
1
+ /**
2
+ * The bundled shell companions emit awk's open-file diagnostic. GNU awk and
3
+ * the BSD awk shipped by macOS format that diagnostic differently. Prefer
4
+ * the executable selected by PATH; macOS runners can put GNU awk ahead of the
5
+ * system awk.
6
+ */
7
+ export declare function usesGnuAwk(pathValue: string | undefined, platform: NodeJS.Platform): boolean;
1
8
  export declare function awkCannotOpenFileDiagnostic(filename: string, reason: string, sourceLine?: number, platform?: NodeJS.Platform, pathValue?: string | undefined): string;
2
9
  //# sourceMappingURL=awk-diagnostics.d.ts.map
@@ -6,7 +6,7 @@ import * as path from "node:path";
6
6
  * the executable selected by PATH; macOS runners can put GNU awk ahead of the
7
7
  * system awk.
8
8
  */
9
- function usesGnuAwk(pathValue, platform) {
9
+ export function usesGnuAwk(pathValue, platform) {
10
10
  for (const directory of (pathValue ?? "/usr/bin:/bin").split(path.delimiter)) {
11
11
  if (!directory)
12
12
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.335.1-beta.0",
3
+ "version": "5.336.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {