@indigoai-us/hq-cloud 6.13.0 → 6.13.2
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/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +30 -0
- package/dist/bin/sync-runner.d.ts +2 -0
- package/dist/bin/sync-runner.d.ts.map +1 -1
- package/dist/bin/sync-runner.js +35 -2
- package/dist/bin/sync-runner.js.map +1 -1
- package/dist/bin/sync-runner.test.js +14 -0
- package/dist/bin/sync-runner.test.js.map +1 -1
- package/dist/cli/reindex.d.ts.map +1 -1
- package/dist/cli/reindex.js +49 -5
- package/dist/cli/reindex.js.map +1 -1
- package/dist/cli/reindex.test.js +55 -0
- package/dist/cli/reindex.test.js.map +1 -1
- package/dist/ignore.d.ts.map +1 -1
- package/dist/ignore.js +10 -0
- package/dist/ignore.js.map +1 -1
- package/dist/ignore.test.js +29 -6
- package/dist/ignore.test.js.map +1 -1
- package/dist/journal.d.ts +7 -1
- package/dist/journal.d.ts.map +1 -1
- package/dist/journal.js +32 -1
- package/dist/journal.js.map +1 -1
- package/dist/journal.test.js +34 -1
- package/dist/journal.test.js.map +1 -1
- package/dist/operation-lock.d.ts +31 -0
- package/dist/operation-lock.d.ts.map +1 -1
- package/dist/operation-lock.js +75 -2
- package/dist/operation-lock.js.map +1 -1
- package/dist/operation-lock.test.js +81 -1
- package/dist/operation-lock.test.js.map +1 -1
- package/dist/skill-telemetry.d.ts.map +1 -1
- package/dist/skill-telemetry.js +7 -3
- package/dist/skill-telemetry.js.map +1 -1
- package/dist/skill-telemetry.test.js +12 -0
- package/dist/skill-telemetry.test.js.map +1 -1
- package/package.json +1 -1
- package/src/bin/sync-runner.test.ts +20 -0
- package/src/bin/sync-runner.ts +57 -14
- package/src/cli/reindex.test.ts +60 -0
- package/src/cli/reindex.ts +58 -8
- package/src/ignore.test.ts +32 -6
- package/src/ignore.ts +11 -0
- package/src/journal.test.ts +44 -0
- package/src/journal.ts +36 -1
- package/src/operation-lock.test.ts +93 -0
- package/src/operation-lock.ts +78 -2
- package/src/skill-telemetry.test.ts +24 -0
- package/src/skill-telemetry.ts +6 -3
package/src/operation-lock.ts
CHANGED
|
@@ -103,6 +103,12 @@ import * as path from "path";
|
|
|
103
103
|
/** Process exit code used when an operation is refused because the lock is held. */
|
|
104
104
|
export const OPERATION_LOCKED_EXIT = 17;
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Process exit code used when an operation is refused because the lock's state
|
|
108
|
+
* directory is not writable (a permission-class fs error, not a live holder).
|
|
109
|
+
*/
|
|
110
|
+
export const OPERATION_LOCK_UNWRITABLE_EXIT = 18;
|
|
111
|
+
|
|
106
112
|
export interface LockInfo {
|
|
107
113
|
pid: number;
|
|
108
114
|
command: string;
|
|
@@ -128,6 +134,36 @@ export class OperationLockedError extends Error {
|
|
|
128
134
|
}
|
|
129
135
|
}
|
|
130
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Thrown by `acquireOperationLock*` when the per-root lock CANNOT BE CREATED
|
|
139
|
+
* because its state directory is not writable — a permission-class fs error
|
|
140
|
+
* (`EPERM` / `EACCES` / `EROFS`) on the lock dir or its temp file, NOT a live
|
|
141
|
+
* holder. Distinct from {@link OperationLockedError}: nothing is holding the
|
|
142
|
+
* lock; this process simply cannot write there — e.g. `~/.hq` owned by another
|
|
143
|
+
* user (created by a past `sudo` run), a macOS privacy/security restriction
|
|
144
|
+
* (which surfaces as `EPERM: operation not permitted`), or a read-only volume.
|
|
145
|
+
* Callers turn this into a clear, actionable message + clean exit rather than a
|
|
146
|
+
* raw uncaught `fs` crash (HQ-CLI-2).
|
|
147
|
+
*/
|
|
148
|
+
export class OperationLockUnwritableError extends Error {
|
|
149
|
+
constructor(
|
|
150
|
+
public readonly lockDir: string,
|
|
151
|
+
public readonly cause: NodeJS.ErrnoException,
|
|
152
|
+
) {
|
|
153
|
+
super(
|
|
154
|
+
`Cannot create the HQ operation lock in "${lockDir}" ` +
|
|
155
|
+
`(${cause.code}: ${cause.message}). That directory is not writable, so ` +
|
|
156
|
+
`HQ can't guard this operation against a concurrent run. Likely causes: ` +
|
|
157
|
+
`it is owned by another user (e.g. created by a past "sudo" command), a ` +
|
|
158
|
+
`macOS privacy/security restriction, or a read-only volume. To fix: make ` +
|
|
159
|
+
`it writable (e.g. "sudo chown -R $(whoami) ~/.hq"), or point HQ at a ` +
|
|
160
|
+
`writable state directory with HQ_STATE_DIR=<dir>. To bypass the lock for ` +
|
|
161
|
+
`a single run (drops concurrency protection), set HQ_DISABLE_OP_LOCK=1.`,
|
|
162
|
+
);
|
|
163
|
+
this.name = "OperationLockUnwritableError";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
131
167
|
export interface LockHandle {
|
|
132
168
|
/** Absolute path of the lock file. */
|
|
133
169
|
readonly path: string;
|
|
@@ -363,10 +399,42 @@ function disabledHandle(hqRoot: string, command: string): LockHandle {
|
|
|
363
399
|
return { ...NOOP_HANDLE_BASE, path: "", info };
|
|
364
400
|
}
|
|
365
401
|
|
|
402
|
+
/**
|
|
403
|
+
* Permission-class fs error codes that mean the lock's state directory itself is
|
|
404
|
+
* unusable for THIS process (not a transient/EEXIST race). macOS surfaces the
|
|
405
|
+
* restricted-location case as `EPERM`; Linux as `EACCES`; a read-only mount as
|
|
406
|
+
* `EROFS`.
|
|
407
|
+
*/
|
|
408
|
+
const UNWRITABLE_LOCK_CODES = new Set(["EPERM", "EACCES", "EROFS"]);
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* If `err` is a permission-class fs error against the lock dir, rethrow it as an
|
|
412
|
+
* actionable {@link OperationLockUnwritableError}; otherwise rethrow it
|
|
413
|
+
* unchanged. Always throws (return type `never`).
|
|
414
|
+
*
|
|
415
|
+
* Exported for unit testing: ESM module namespaces can't be spied, so the
|
|
416
|
+
* classification decision is verified directly here rather than by mocking
|
|
417
|
+
* `fs.openSync` (see operation-lock.test.ts, HQ-CLI-2).
|
|
418
|
+
*/
|
|
419
|
+
export function rethrowLockCreateError(err: unknown, lockDir: string): never {
|
|
420
|
+
const e = err as NodeJS.ErrnoException | null;
|
|
421
|
+
if (e && typeof e.code === "string" && UNWRITABLE_LOCK_CODES.has(e.code)) {
|
|
422
|
+
throw new OperationLockUnwritableError(lockDir, e);
|
|
423
|
+
}
|
|
424
|
+
throw err;
|
|
425
|
+
}
|
|
426
|
+
|
|
366
427
|
/** Build the lock payload + ensure the locks dir exists. */
|
|
367
428
|
function prepareLock(hqRoot: string, command: string): { p: string; info: LockInfo; payload: string } {
|
|
368
429
|
const p = lockPathFor(hqRoot);
|
|
369
|
-
|
|
430
|
+
const dir = path.dirname(p);
|
|
431
|
+
try {
|
|
432
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
433
|
+
} catch (err) {
|
|
434
|
+
// The state dir (e.g. ~/.hq) can't be created — surface it as an actionable
|
|
435
|
+
// unwritable-lock error rather than a raw mkdir crash.
|
|
436
|
+
rethrowLockCreateError(err, dir);
|
|
437
|
+
}
|
|
370
438
|
const info: LockInfo = {
|
|
371
439
|
pid: process.pid,
|
|
372
440
|
command,
|
|
@@ -381,7 +449,15 @@ function writeLockTemp(dir: string, payload: string): string {
|
|
|
381
449
|
dir,
|
|
382
450
|
`.operation-lock.${process.pid}.${Date.now()}.${crypto.randomBytes(6).toString("hex")}.tmp`,
|
|
383
451
|
);
|
|
384
|
-
|
|
452
|
+
let fd: number;
|
|
453
|
+
try {
|
|
454
|
+
fd = fs.openSync(tmp, "wx", 0o600);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
// The locks dir isn't writable (e.g. macOS EPERM / Linux EACCES on a
|
|
457
|
+
// root-owned or restricted ~/.hq) — surface an actionable error, not a raw
|
|
458
|
+
// `EPERM: operation not permitted, open` crash (HQ-CLI-2).
|
|
459
|
+
rethrowLockCreateError(err, dir);
|
|
460
|
+
}
|
|
385
461
|
let closed = false;
|
|
386
462
|
try {
|
|
387
463
|
fs.writeSync(fd, payload);
|
|
@@ -226,6 +226,30 @@ describe("extractCodexSkillToolEvents (Codex model-driven SKILL.md reads)", () =
|
|
|
226
226
|
expect(ev.source).toBe("model");
|
|
227
227
|
});
|
|
228
228
|
|
|
229
|
+
it("matches normal Codex skill paths without backtracking ambiguity", () => {
|
|
230
|
+
expect(
|
|
231
|
+
extractCodexSkillToolEvents(
|
|
232
|
+
execEnd("cat /home/u/.claude/skills/hq/land/SKILL.md"),
|
|
233
|
+
ctx,
|
|
234
|
+
)[0].skill,
|
|
235
|
+
).toBe("land");
|
|
236
|
+
expect(
|
|
237
|
+
extractCodexSkillToolEvents(
|
|
238
|
+
execEnd("cat /home/u/.claude/skills/a/b/c/SKILL.md"),
|
|
239
|
+
ctx,
|
|
240
|
+
)[0].skill,
|
|
241
|
+
).toBe("c");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("returns quickly for pathological non-matching skills paths", () => {
|
|
245
|
+
const cmd = "/home/u/.claude/skills/" + "a/".repeat(2000) + "z z";
|
|
246
|
+
const started = Date.now();
|
|
247
|
+
const events = extractCodexSkillToolEvents(execEnd(cmd), ctx);
|
|
248
|
+
const elapsedMs = Date.now() - started;
|
|
249
|
+
expect(events).toEqual([]);
|
|
250
|
+
expect(elapsedMs).toBeLessThan(200);
|
|
251
|
+
});
|
|
252
|
+
|
|
229
253
|
it("absolute and relative SKILL.md paths both resolve", () => {
|
|
230
254
|
expect(
|
|
231
255
|
extractCodexSkillToolEvents(execEnd("cat /abs/path/.agents/skills/quiz/SKILL.md"), ctx)[0].skill,
|
package/src/skill-telemetry.ts
CHANGED
|
@@ -342,10 +342,11 @@ export function extractCodexSkillEvents(
|
|
|
342
342
|
* may be nested arbitrarily deep (`.agents/skills/…`, `.codex/skills/hq/…`),
|
|
343
343
|
* and `<name>` is always the directory immediately above SKILL.md — captured
|
|
344
344
|
* as the last segment so the bridge's `skills/hq/<name>/` layout resolves to
|
|
345
|
-
* `<name>`, not `hq`. The
|
|
346
|
-
*
|
|
345
|
+
* `<name>`, not `hq`. The segment classes deliberately exclude `/` so segment
|
|
346
|
+
* boundaries are unambiguous and non-matching `skills/...` paths cannot
|
|
347
|
+
* catastrophically backtrack. */
|
|
347
348
|
const CODEX_SKILL_FILE =
|
|
348
|
-
/(?:^|\/)skills\/(?:[
|
|
349
|
+
/(?:^|\/)skills\/(?:[^/\s'"]+\/)*?([^/\s'"]+)\/SKILL\.md\b/;
|
|
349
350
|
|
|
350
351
|
/** Pull the shell command string out of a Codex `exec_command_end` `command`,
|
|
351
352
|
* which is `["/bin/zsh", "-lc", "<cmd>"]` (array) on the runtimes we see, but
|
|
@@ -488,6 +489,8 @@ export function extractCodexSkillToolEvents(
|
|
|
488
489
|
const exec = codexExecParams(obj, payload, ctx);
|
|
489
490
|
if (!exec) return [];
|
|
490
491
|
const { cmd } = exec;
|
|
492
|
+
// Real exec commands are tiny; avoid running regexes over pathological rows.
|
|
493
|
+
if (cmd.length > 100_000) return [];
|
|
491
494
|
const m = CODEX_SKILL_FILE.exec(cmd);
|
|
492
495
|
if (!m) return [];
|
|
493
496
|
// Confirm the exec is a read of the skill file, not a write to it. Codex's own
|