@indigoai-us/hq-cli 5.101.0 → 5.101.1

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,20 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.101.1] — 2026-08-14
6
+
7
+ ### Fixed
8
+
9
+ - `hq core rebuild-index threads` no longer crashes with an `ENOENT` stat error
10
+ when a `workspace/threads/T-*.json` entry that the directory scan listed can no
11
+ longer be resolved — removed mid-scan by a concurrent writer (HQ Sync, another
12
+ session, or `archive-old-threads`) or left as a dangling symlink. The renderer
13
+ now stats each file exactly once before sorting (a Schwartzian transform rather
14
+ than statting inside the sort comparator), skips entries that vanished (logging
15
+ a single summary line by basename), and still fails loudly on a genuine stat
16
+ error such as `EACCES`. Both `INDEX.md` and `recent.md` are regenerated as
17
+ before. Sentry 7669694322 (HQ-CLI-P).
18
+
5
19
  ## [5.101.0] — 2026-08-13
6
20
 
7
21
  ### Added
@@ -31,5 +31,24 @@ export declare function projectStatus(root: string, project: string, prdPath: st
31
31
  export declare function basename(file: string): string;
32
32
  export declare function isHidden(name: string): boolean;
33
33
  export declare function mtime(file: string): number;
34
+ /**
35
+ * Sort key for newest-first ordering: the file's modification time in
36
+ * nanoseconds. Stat the file exactly once, before sorting — never from inside a
37
+ * comparator, where a throw aborts `Array.sort` and an mtime that changes
38
+ * mid-sort makes the comparator inconsistent.
39
+ *
40
+ * Returns `undefined` when the entry a directory scan just listed can no longer
41
+ * be resolved: it was removed between the `readdirSync` snapshot and this stat
42
+ * (a time-of-check-to-time-of-use race in a hot, multi-writer directory), or it
43
+ * is a symlink whose target is missing (`statSync` follows symlinks). Callers
44
+ * drop such entries — mirroring how `immediateEntries` and `readJson` already
45
+ * tolerate a vanished file — so the render never crashes on a benign race.
46
+ *
47
+ * Any other stat failure (EACCES, EIO, …) is a genuine fault and is rethrown
48
+ * with the offending file's BASENAME attached, never its absolute path, which
49
+ * must not reach error reporting such as Sentry. The original errno `code` is
50
+ * preserved so upstream error classification is unaffected.
51
+ */
52
+ export declare function sortKeyMtimeNs(file: string): bigint | undefined;
34
53
  export declare function tempDirectory(prefix: string): string;
35
54
  //# sourceMappingURL=shared.d.ts.map
@@ -120,6 +120,38 @@ export function mtime(file) { try {
120
120
  catch {
121
121
  return 0;
122
122
  } }
123
+ /**
124
+ * Sort key for newest-first ordering: the file's modification time in
125
+ * nanoseconds. Stat the file exactly once, before sorting — never from inside a
126
+ * comparator, where a throw aborts `Array.sort` and an mtime that changes
127
+ * mid-sort makes the comparator inconsistent.
128
+ *
129
+ * Returns `undefined` when the entry a directory scan just listed can no longer
130
+ * be resolved: it was removed between the `readdirSync` snapshot and this stat
131
+ * (a time-of-check-to-time-of-use race in a hot, multi-writer directory), or it
132
+ * is a symlink whose target is missing (`statSync` follows symlinks). Callers
133
+ * drop such entries — mirroring how `immediateEntries` and `readJson` already
134
+ * tolerate a vanished file — so the render never crashes on a benign race.
135
+ *
136
+ * Any other stat failure (EACCES, EIO, …) is a genuine fault and is rethrown
137
+ * with the offending file's BASENAME attached, never its absolute path, which
138
+ * must not reach error reporting such as Sentry. The original errno `code` is
139
+ * preserved so upstream error classification is unaffected.
140
+ */
141
+ export function sortKeyMtimeNs(file) {
142
+ try {
143
+ return fs.statSync(file, { bigint: true }).mtimeNs;
144
+ }
145
+ catch (error) {
146
+ const code = error?.code;
147
+ if (code === "ENOENT" || code === "ENOTDIR")
148
+ return undefined;
149
+ const wrapped = new Error(`failed to stat ${basename(file)} (${code ?? "unknown error"})`);
150
+ if (code !== undefined)
151
+ wrapped.code = code;
152
+ throw wrapped;
153
+ }
154
+ }
123
155
  // Kept exported for primitive tests and consumers that need a temp base without
124
156
  // relying on an application-specific fixture location.
125
157
  export function tempDirectory(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); }
@@ -1,19 +1,40 @@
1
1
  import * as fs from "fs";
2
- import { at, log, readJson, sanitize, timestamp, write } from "./shared.js";
2
+ import { at, basename, log, readJson, sanitize, sortKeyMtimeNs, timestamp, write } from "./shared.js";
3
3
  export function renderThreads(context, args = []) {
4
4
  const mode = args[0] ?? "--index";
5
5
  const directory = at(context.root, "workspace/threads");
6
6
  fs.mkdirSync(directory, { recursive: true });
7
7
  // Match `ls -t`: newest filesystem modification time first. `updated_at` is
8
8
  // rendered metadata only, and must not influence the order.
9
+ //
10
+ // Stat every listed file exactly once, BEFORE sorting (a Schwartzian
11
+ // transform), never from inside the comparator. workspace/threads is a hot,
12
+ // multi-writer directory (HQ Sync reconciliation, concurrent agent sessions,
13
+ // and `archive-old-threads` renaming T-*.json out of it), so an entry
14
+ // readdirSync just snapshotted can vanish before it is stat'ed. Statting
15
+ // inside the comparator turned that time-of-check-to-time-of-use window — and
16
+ // any dangling symlink — into an ENOENT that aborted the whole command before
17
+ // either file was written. Precompute the key, drop entries that no longer
18
+ // resolve (as `readJson` already drops unreadable files), then compare only
19
+ // precomputed keys so the comparator can neither throw nor be inconsistent.
20
+ const skipped = [];
9
21
  const files = fs.readdirSync(directory)
10
22
  .filter((name) => /^T-.*\.json$/.test(name) && !name.endsWith(".changeset.json"))
11
23
  .map((name) => `${directory}/${name}`)
12
- .sort((a, b) => {
13
- const aTime = fs.statSync(a, { bigint: true }).mtimeNs;
14
- const bTime = fs.statSync(b, { bigint: true }).mtimeNs;
15
- return bTime > aTime ? 1 : bTime < aTime ? -1 : 0;
16
- });
24
+ .map((file) => ({ file, key: sortKeyMtimeNs(file) }))
25
+ .filter((entry) => {
26
+ if (entry.key === undefined) {
27
+ skipped.push(basename(entry.file));
28
+ return false;
29
+ }
30
+ return true;
31
+ })
32
+ .sort((a, b) => (b.key > a.key ? 1 : b.key < a.key ? -1 : 0))
33
+ .map((entry) => entry.file);
34
+ if (skipped.length > 0) {
35
+ const shown = skipped.slice(0, 10).join(", ");
36
+ log(context, `rebuild-threads-index: skipped ${skipped.length} thread file(s) that vanished during the scan: ${shown}${skipped.length > 10 ? ", …" : ""}`);
37
+ }
17
38
  const rows = files.flatMap((file) => {
18
39
  const data = readJson(file);
19
40
  if (!data)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.101.0",
3
+ "version": "5.101.1",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -29,7 +29,7 @@
29
29
  "dependencies": {
30
30
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
31
31
  "@aws-sdk/client-s3": "^3.1049.0",
32
- "@indigoai-us/hq-cloud": "^6.14.50",
32
+ "@indigoai-us/hq-cloud": "~6.15.0",
33
33
  "@indigoai-us/hq-onboarding": "^0.1.0",
34
34
  "@sentry/node": "^10.49.0",
35
35
  "@tobilu/qmd": "2.5.3",