@indigoai-us/hq-cli 5.337.0 → 5.339.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 +7 -0
- package/assets/scaffold/core/scripts/migrate-policy-triggers.sh +81 -6
- package/assets/scaffold/core/scripts/repo-run-registry.sh +26 -4
- package/dist/commands/core-migrate-policy-triggers-native.js +132 -9
- package/dist/commands/core-session-journal-native.d.ts +5 -1
- package/dist/commands/core-session-journal-native.js +12 -7
- package/dist/fast-core.d.ts +6 -0
- package/dist/fast-core.js +72 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.339.0] — 2026-10-03
|
|
6
|
+
- Reduce cold-start imports for the `checkpoint-stop-gate` and `session-journal` native fast routes.
|
|
7
|
+
|
|
8
|
+
## [5.338.0] — 2026-10-03
|
|
9
|
+
- Preserve policy-trigger mutual exclusion without `flock` and honor configured stale-run thresholds without `yq`.
|
|
10
|
+
- Add a manual macOS VM parity workflow for checking HQ CLI behavior in Tart.
|
|
11
|
+
|
|
5
12
|
## [5.337.0] — 2026-10-03
|
|
6
13
|
- QMD index updates now share the cross-process writer lock and skip bounded SQLite contention so a later sync can catch up.
|
|
7
14
|
- Self-update now checks the Windows npm prefix that stores command shims, so an unwritable prefix is detected before npm runs.
|
|
@@ -124,8 +124,61 @@ fi
|
|
|
124
124
|
COOLDOWN_SECONDS="${HQ_MIGRATE_POLICY_TRIGGERS_COOLDOWN_SECONDS:-3600}"
|
|
125
125
|
STAMP=""
|
|
126
126
|
LOCK=""
|
|
127
|
+
LOCK_FALLBACK_DIR=""
|
|
128
|
+
LOCK_FALLBACK_HELD=0
|
|
129
|
+
LOCK_FALLBACK_OWNER=""
|
|
130
|
+
LOCK_FALLBACK_STALE_SECONDS=86400
|
|
127
131
|
STATE_READY=0
|
|
128
132
|
|
|
133
|
+
release_fallback_lock() {
|
|
134
|
+
[ "$LOCK_FALLBACK_HELD" = "1" ] || return 0
|
|
135
|
+
[ "$(cat "$LOCK_FALLBACK_DIR/owner" 2>/dev/null)" = "$LOCK_FALLBACK_OWNER" ] || return 0
|
|
136
|
+
rm -f "$LOCK_FALLBACK_DIR/owner" 2>/dev/null || return 0
|
|
137
|
+
rmdir "$LOCK_FALLBACK_DIR" 2>/dev/null || true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fallback_lock_is_stale() {
|
|
141
|
+
local lock_dir="$1" modified now age
|
|
142
|
+
modified="$(stat -c '%Y' "$lock_dir" 2>/dev/null || stat -f '%m' "$lock_dir" 2>/dev/null)"
|
|
143
|
+
case "$modified" in ''|*[!0-9]*) return 1 ;; esac
|
|
144
|
+
now="$(date -u '+%s')"
|
|
145
|
+
age=$((now - modified))
|
|
146
|
+
[ "$age" -ge "$LOCK_FALLBACK_STALE_SECONDS" ]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
reclaim_stale_fallback_lock() {
|
|
150
|
+
local stale_dir="${LOCK_FALLBACK_DIR}.stale.$$.$RANDOM" owner_present=0 owner_value=""
|
|
151
|
+
restore_stale_dir() {
|
|
152
|
+
# Never let mv place the quarantined directory inside a replacement lock.
|
|
153
|
+
# A new holder may have created the path after our rename-aside.
|
|
154
|
+
if [ -e "$LOCK_FALLBACK_DIR" ] || [ -L "$LOCK_FALLBACK_DIR" ]; then return 1; fi
|
|
155
|
+
mv "$stale_dir" "$LOCK_FALLBACK_DIR" 2>/dev/null
|
|
156
|
+
}
|
|
157
|
+
[ -d "$LOCK_FALLBACK_DIR" ] || return 1
|
|
158
|
+
fallback_lock_is_stale "$LOCK_FALLBACK_DIR" || return 1
|
|
159
|
+
# Rename the observed lock aside atomically. Recheck the moved directory so
|
|
160
|
+
# a stale decision is never used to remove a replacement lock at the path.
|
|
161
|
+
mv "$LOCK_FALLBACK_DIR" "$stale_dir" 2>/dev/null || return 1
|
|
162
|
+
if fallback_lock_is_stale "$stale_dir"; then
|
|
163
|
+
if [ -e "$stale_dir/owner" ]; then
|
|
164
|
+
owner_present=1
|
|
165
|
+
owner_value="$(cat "$stale_dir/owner" 2>/dev/null)"
|
|
166
|
+
rm -f "$stale_dir/owner" 2>/dev/null || {
|
|
167
|
+
restore_stale_dir || true
|
|
168
|
+
return 1
|
|
169
|
+
}
|
|
170
|
+
fi
|
|
171
|
+
if rmdir "$stale_dir" 2>/dev/null; then
|
|
172
|
+
return 0
|
|
173
|
+
fi
|
|
174
|
+
if [ "$owner_present" = "1" ]; then
|
|
175
|
+
(set -o noclobber; printf '%s\n' "$owner_value" >"$stale_dir/owner") 2>/dev/null || true
|
|
176
|
+
fi
|
|
177
|
+
fi
|
|
178
|
+
restore_stale_dir || true
|
|
179
|
+
return 1
|
|
180
|
+
}
|
|
181
|
+
|
|
129
182
|
# Hash the resolved scope rather than using a shared per-root stamp: the
|
|
130
183
|
# default scan deliberately includes tenant-specific directories.
|
|
131
184
|
scope_key() {
|
|
@@ -192,12 +245,34 @@ fi
|
|
|
192
245
|
# A scope-local advisory lock prevents concurrent SessionStart hooks from both
|
|
193
246
|
# observing the same absent/stale stamp. Recheck after acquiring it because a
|
|
194
247
|
# prior holder may have completed while this invocation waited to acquire it.
|
|
195
|
-
if [ "$STATE_READY" = "1" ] && [ "$DRY" != "1" ]
|
|
196
|
-
if
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
248
|
+
if [ "$STATE_READY" = "1" ] && [ "$DRY" != "1" ]; then
|
|
249
|
+
if command -v flock >/dev/null 2>&1; then
|
|
250
|
+
if ! exec 9>"$LOCK"; then
|
|
251
|
+
exit 0
|
|
252
|
+
fi
|
|
253
|
+
if ! flock -n 9; then
|
|
254
|
+
exit 0
|
|
255
|
+
fi
|
|
256
|
+
else
|
|
257
|
+
# Atomic directory creation preserves mutual exclusion on stock macOS,
|
|
258
|
+
# which does not ship util-linux flock. An abandoned directory is only
|
|
259
|
+
# reclaimed after a 24-hour age threshold, well above this bounded scan.
|
|
260
|
+
LOCK_FALLBACK_DIR="${LOCK}.d"
|
|
261
|
+
if ! mkdir "$LOCK_FALLBACK_DIR" 2>/dev/null; then
|
|
262
|
+
if reclaim_stale_fallback_lock; then
|
|
263
|
+
mkdir "$LOCK_FALLBACK_DIR" 2>/dev/null || exit 0
|
|
264
|
+
else
|
|
265
|
+
exit 0
|
|
266
|
+
fi
|
|
267
|
+
fi
|
|
268
|
+
LOCK_FALLBACK_HELD=1
|
|
269
|
+
LOCK_FALLBACK_OWNER="$$:$RANDOM:$(date -u '+%s')"
|
|
270
|
+
if ! printf '%s\n' "$LOCK_FALLBACK_OWNER" >"$LOCK_FALLBACK_DIR/owner"; then
|
|
271
|
+
rmdir "$LOCK_FALLBACK_DIR" 2>/dev/null || true
|
|
272
|
+
LOCK_FALLBACK_HELD=0
|
|
273
|
+
exit 0
|
|
274
|
+
fi
|
|
275
|
+
trap release_fallback_lock EXIT
|
|
201
276
|
fi
|
|
202
277
|
if cooldown_blocks_run; then
|
|
203
278
|
exit 0
|
|
@@ -127,10 +127,32 @@ _is_pid_alive() {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
_stale_minutes() {
|
|
130
|
-
if [[ -f "$ORCH_YAML" ]]
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
if [[ -f "$ORCH_YAML" ]]; then
|
|
131
|
+
if command -v yq >/dev/null 2>&1; then
|
|
132
|
+
local v
|
|
133
|
+
v=$(yq eval '.repo_coordination.stale_heartbeat_minutes // 15' "$ORCH_YAML" 2>/dev/null)
|
|
134
|
+
[[ -n "$v" && "$v" != "null" ]] && { echo "$v"; return; }
|
|
135
|
+
else
|
|
136
|
+
# Parse the one scalar this helper needs when yq is unavailable. Do not
|
|
137
|
+
# silently replace a configured timeout with the 15-minute default.
|
|
138
|
+
awk '
|
|
139
|
+
/^repo_coordination:[[:space:]]*(#.*)?$/ { in_repo=1; next }
|
|
140
|
+
in_repo && /^[^[:space:]#]/ { exit }
|
|
141
|
+
in_repo && /^[[:space:]]+stale_heartbeat_minutes:[[:space:]]*/ {
|
|
142
|
+
sub(/^[[:space:]]+stale_heartbeat_minutes:[[:space:]]*/, "")
|
|
143
|
+
sub(/[[:space:]]+#.*$/, "")
|
|
144
|
+
gsub(/^[[:space:]]+|[[:space:]]+$/, "")
|
|
145
|
+
gsub(/^["\047]|["\047]$/, "")
|
|
146
|
+
if ($0 == "" || $0 == "null" || $0 == "~") print 15
|
|
147
|
+
else if ($0 ~ /^[0-9]+$/) print $0
|
|
148
|
+
else print 15
|
|
149
|
+
found=1
|
|
150
|
+
exit
|
|
151
|
+
}
|
|
152
|
+
END { if (!found) print 15 }
|
|
153
|
+
' "$ORCH_YAML"
|
|
154
|
+
return
|
|
155
|
+
fi
|
|
134
156
|
fi
|
|
135
157
|
echo 15
|
|
136
158
|
}
|
|
@@ -7,6 +7,7 @@ import { fileURLToPath } from "node:url";
|
|
|
7
7
|
import { evaluateTrigger } from "../utils/core-eval-trigger.js";
|
|
8
8
|
import { awkCannotOpenFileDiagnostic } from "../lib/core-utils/awk-diagnostics.js";
|
|
9
9
|
export const MIGRATE_POLICY_TRIGGERS_ASSET = "core/scripts/migrate-policy-triggers.sh";
|
|
10
|
+
const FALLBACK_LOCK_STALE_SECONDS = 24 * 60 * 60;
|
|
10
11
|
const ON_LIVE = "[PreToolUse, PostToolUse, UserPromptSubmit, AssistantIntent]";
|
|
11
12
|
const ON_START = "[SessionStart]";
|
|
12
13
|
const TAG_STOP = new Set(("infrastructure consolidated ux safety hq hq-core hq-cli hq-packages " +
|
|
@@ -173,7 +174,7 @@ function unreadablePolicyDiagnostic(filename, error, pathValue) {
|
|
|
173
174
|
const message = code === "EACCES" || code === "EPERM" ? "Permission denied" : "No such file or directory";
|
|
174
175
|
const script = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../assets/scaffold/core/scripts/migrate-policy-triggers.sh");
|
|
175
176
|
const awk = awkCannotOpenFileDiagnostic(filename, message, 1, process.platform, pathValue);
|
|
176
|
-
return `${script}: line
|
|
177
|
+
return `${script}: line 389: ${filename}: ${message}\n${awk.repeat(3)}`;
|
|
177
178
|
}
|
|
178
179
|
function shellGlobMd(fileSystem, directory) {
|
|
179
180
|
try {
|
|
@@ -255,20 +256,141 @@ export function runMigratePolicyTriggers(args, deps) {
|
|
|
255
256
|
if (cooldownBlocks())
|
|
256
257
|
return { code: 0, stdout, stderr };
|
|
257
258
|
let lockFd;
|
|
258
|
-
|
|
259
|
+
let fallbackLockDir;
|
|
260
|
+
let fallbackLockOwner;
|
|
261
|
+
const releaseFallbackLock = () => {
|
|
262
|
+
if (!fallbackLockDir)
|
|
263
|
+
return;
|
|
259
264
|
try {
|
|
260
|
-
|
|
265
|
+
if (fileSystem.readFileSync(path.join(fallbackLockDir, "owner"), "utf8").trim() === fallbackLockOwner) {
|
|
266
|
+
fileSystem.unlinkSync(path.join(fallbackLockDir, "owner"));
|
|
267
|
+
fileSystem.rmdirSync(fallbackLockDir);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
catch { /* preserve the lock if ownership cannot be confirmed */ }
|
|
271
|
+
fallbackLockDir = undefined;
|
|
272
|
+
};
|
|
273
|
+
const reclaimStaleFallbackLock = (directory) => {
|
|
274
|
+
const restoreQuarantined = (staleDirectory) => {
|
|
275
|
+
try {
|
|
276
|
+
fileSystem.lstatSync(directory);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
if (error.code !== "ENOENT")
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
fileSystem.renameSync(staleDirectory, directory);
|
|
285
|
+
}
|
|
286
|
+
catch { /* preserve the quarantine if restoration fails */ }
|
|
287
|
+
};
|
|
288
|
+
const age = (candidate) => {
|
|
289
|
+
try {
|
|
290
|
+
return Math.floor(deps.now().getTime() / 1000) - Math.floor(fileSystem.statSync(candidate).mtimeMs / 1000);
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
if ((age(directory) ?? -1) < FALLBACK_LOCK_STALE_SECONDS)
|
|
297
|
+
return false;
|
|
298
|
+
const staleDirectory = `${directory}.stale.${process.pid}.${Math.random().toString(16).slice(2)}`;
|
|
299
|
+
try {
|
|
300
|
+
fileSystem.renameSync(directory, staleDirectory);
|
|
261
301
|
}
|
|
262
302
|
catch {
|
|
263
|
-
return
|
|
303
|
+
return false;
|
|
264
304
|
}
|
|
265
|
-
const
|
|
266
|
-
if (
|
|
267
|
-
|
|
268
|
-
|
|
305
|
+
const staleAge = age(staleDirectory);
|
|
306
|
+
if (staleAge !== undefined && staleAge >= FALLBACK_LOCK_STALE_SECONDS) {
|
|
307
|
+
let ownerBytes;
|
|
308
|
+
try {
|
|
309
|
+
ownerBytes = fileSystem.readFileSync(path.join(staleDirectory, "owner"), "utf8");
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
if (error.code !== "ENOENT") {
|
|
313
|
+
restoreQuarantined(staleDirectory);
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (ownerBytes !== undefined) {
|
|
318
|
+
try {
|
|
319
|
+
fileSystem.unlinkSync(path.join(staleDirectory, "owner"));
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
restoreQuarantined(staleDirectory);
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
fileSystem.rmdirSync(staleDirectory);
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
if (ownerBytes !== undefined) {
|
|
332
|
+
try {
|
|
333
|
+
fileSystem.writeFileSync(path.join(staleDirectory, "owner"), ownerBytes, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
334
|
+
}
|
|
335
|
+
catch { /* keep the quarantined directory rather than overwrite a changed marker */ }
|
|
336
|
+
}
|
|
337
|
+
restoreQuarantined(staleDirectory);
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
restoreQuarantined(staleDirectory);
|
|
342
|
+
return false;
|
|
343
|
+
};
|
|
344
|
+
if (stateReady && !dryRun) {
|
|
345
|
+
if (executableInPath("flock", deps.env, fileSystem)) {
|
|
346
|
+
try {
|
|
347
|
+
lockFd = fileSystem.openSync(lock, "w");
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return { code: 0, stdout, stderr };
|
|
351
|
+
}
|
|
352
|
+
const lockResult = (deps.runFlock ?? runNonblockingFlock)(lockFd, deps.cwd, deps.env.PATH ?? "");
|
|
353
|
+
if (lockResult.error || lockResult.status !== 0) {
|
|
354
|
+
fileSystem.closeSync(lockFd);
|
|
355
|
+
return { code: 0, stdout, stderr };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
fallbackLockDir = `${lock}.d`;
|
|
360
|
+
try {
|
|
361
|
+
fileSystem.mkdirSync(fallbackLockDir);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
if (!reclaimStaleFallbackLock(fallbackLockDir)) {
|
|
365
|
+
fallbackLockDir = undefined;
|
|
366
|
+
return { code: 0, stdout, stderr };
|
|
367
|
+
}
|
|
368
|
+
try {
|
|
369
|
+
fileSystem.mkdirSync(fallbackLockDir);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
fallbackLockDir = undefined;
|
|
373
|
+
return { code: 0, stdout, stderr };
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
fallbackLockOwner = `${process.pid}:${Math.random().toString(36).slice(2)}:${nowSeconds()}`;
|
|
377
|
+
try {
|
|
378
|
+
fileSystem.writeFileSync(path.join(fallbackLockDir, "owner"), `${fallbackLockOwner}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
try {
|
|
382
|
+
fileSystem.rmdirSync(fallbackLockDir);
|
|
383
|
+
}
|
|
384
|
+
catch { /* preserve a lock if cleanup is unsafe */ }
|
|
385
|
+
fallbackLockDir = undefined;
|
|
386
|
+
fallbackLockOwner = undefined;
|
|
387
|
+
return { code: 0, stdout, stderr };
|
|
388
|
+
}
|
|
269
389
|
}
|
|
270
390
|
if (cooldownBlocks()) {
|
|
271
|
-
|
|
391
|
+
if (lockFd !== undefined)
|
|
392
|
+
fileSystem.closeSync(lockFd);
|
|
393
|
+
releaseFallbackLock();
|
|
272
394
|
return { code: 0, stdout, stderr };
|
|
273
395
|
}
|
|
274
396
|
}
|
|
@@ -363,6 +485,7 @@ export function runMigratePolicyTriggers(args, deps) {
|
|
|
363
485
|
finally {
|
|
364
486
|
if (lockFd !== undefined)
|
|
365
487
|
fileSystem.closeSync(lockFd);
|
|
488
|
+
releaseFallbackLock();
|
|
366
489
|
}
|
|
367
490
|
return { code: errorCode, stdout, stderr };
|
|
368
491
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
type SessionHelperRuntimeOverrides = {
|
|
2
|
+
env?: NodeJS.ProcessEnv;
|
|
3
|
+
now?: () => Date;
|
|
4
|
+
};
|
|
2
5
|
export type SessionJournalOperationResult = {
|
|
3
6
|
code: number;
|
|
4
7
|
stdout: Buffer;
|
|
@@ -8,4 +11,5 @@ export type SessionJournalOperationResult = {
|
|
|
8
11
|
export declare function runSessionJournalOperation(args: readonly string[], _stdin: string, cwd: string, hqRoot: string | undefined, dependencies?: SessionHelperRuntimeOverrides): SessionJournalOperationResult;
|
|
9
12
|
/** Native implementation of the fail-soft session journal helper. */
|
|
10
13
|
export declare function runSessionJournalFast(args: readonly string[], stdin: string, cwd: string, hqRoot: string | undefined, dependencies?: SessionHelperRuntimeOverrides): number;
|
|
14
|
+
export {};
|
|
11
15
|
//# sourceMappingURL=core-session-journal-native.d.ts.map
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import { resolveBundledAsset } from "../utils/hq-roots-core.js";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
5
4
|
const USAGE = `session-journal.sh: usage:
|
|
6
5
|
session-journal.sh write "<title>" [--files f1,f2,...] [--body-file PATH] [--session KEY]
|
|
7
6
|
session-journal.sh list [--date YYYY-MM-DD]
|
|
@@ -56,6 +55,7 @@ function yamlDoubleQuoted(value) {
|
|
|
56
55
|
}
|
|
57
56
|
const warning = (message) => { process.stderr.write(`session-journal: ${message}\n`); };
|
|
58
57
|
const journalDir = (root, date) => path.join(root, "workspace/threads/journal", date);
|
|
58
|
+
const sessionJournalAsset = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../assets/scaffold/core/scripts/session-journal.sh");
|
|
59
59
|
const hasFile = (file) => {
|
|
60
60
|
try {
|
|
61
61
|
return fs.statSync(file).isFile();
|
|
@@ -190,11 +190,13 @@ function runToolCounter(root, action, sessionArg, env, now, writers) {
|
|
|
190
190
|
const counter = path.join(dir, `.tool-count-${key}`);
|
|
191
191
|
const lock = `${counter}.lock`;
|
|
192
192
|
const reportDirectoryWriteFailure = (error, line) => {
|
|
193
|
-
|
|
194
|
-
if (code !== "EISDIR")
|
|
193
|
+
if (error.code !== "EISDIR")
|
|
195
194
|
return;
|
|
196
|
-
|
|
197
|
-
|
|
195
|
+
if (!fs.existsSync(sessionJournalAsset)) {
|
|
196
|
+
throw new Error("Bundled asset is missing from the installed package: assets/scaffold/core/scripts/session-journal.sh\n" +
|
|
197
|
+
"This is a packaging fault — the file exists in source but was not published.");
|
|
198
|
+
}
|
|
199
|
+
writers.stderr(`${sessionJournalAsset}: line ${line}: ${counter}: Is a directory\n`);
|
|
198
200
|
};
|
|
199
201
|
if (action === "increment") {
|
|
200
202
|
withCounterLock(lock, () => {
|
|
@@ -243,7 +245,10 @@ export function runSessionJournalFast(args, stdin, cwd, hqRoot, dependencies = {
|
|
|
243
245
|
}
|
|
244
246
|
function executeSessionJournal(args, cwd, hqRoot, dependencies, writers) {
|
|
245
247
|
const warning = (message) => { writers.stderr(`session-journal: ${message}\n`); };
|
|
246
|
-
const runtime =
|
|
248
|
+
const runtime = {
|
|
249
|
+
env: dependencies.env ?? process.env,
|
|
250
|
+
now: dependencies.now ?? (() => new Date()),
|
|
251
|
+
};
|
|
247
252
|
const today = () => runtime.now().toISOString().slice(0, 10);
|
|
248
253
|
const nowIso = () => runtime.now().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
249
254
|
const nowHhmm = () => runtime.now().toISOString().slice(11, 16) + "Z";
|
package/dist/fast-core.d.ts
CHANGED
|
@@ -14,4 +14,10 @@ export declare function readStdinIfPiped(stream: NodeJS.ReadStream): Promise<str
|
|
|
14
14
|
* doing anything.
|
|
15
15
|
*/
|
|
16
16
|
export declare function runFastCore(argv: readonly string[], parsed?: ReturnType<typeof parseFastCore>): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve valid explicit roots for the two measured hook routes without loading
|
|
19
|
+
* the general root resolver. Anything uncertain falls through to that resolver,
|
|
20
|
+
* which retains its diagnostics and cwd-discovery behavior.
|
|
21
|
+
*/
|
|
22
|
+
export declare function resolveExplicitFastLiveRoot(hqRootArg: string | undefined, env: NodeJS.ProcessEnv): string | undefined;
|
|
17
23
|
//# sourceMappingURL=fast-core.d.ts.map
|
package/dist/fast-core.js
CHANGED
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
* entries use the bundled-script runner; native entries import their one handler
|
|
7
7
|
* module. The registration in `core.ts` remains the complete fallback path.
|
|
8
8
|
*/
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
9
12
|
import { parseFastCore } from "./fast-scaffold.js";
|
|
13
|
+
const FAST_ROUTE_PROFILE_TOKEN = "hq-fast-route";
|
|
14
|
+
function wantsFastRouteProfile(env) {
|
|
15
|
+
return env.NODE_DEBUG === FAST_ROUTE_PROFILE_TOKEN;
|
|
16
|
+
}
|
|
10
17
|
export async function readStdinIfPiped(stream) {
|
|
11
18
|
if (stream.isTTY)
|
|
12
19
|
return "";
|
|
@@ -23,6 +30,14 @@ export async function readStdinIfPiped(stream) {
|
|
|
23
30
|
export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
24
31
|
if (!parsed)
|
|
25
32
|
return;
|
|
33
|
+
const profile = wantsFastRouteProfile(process.env);
|
|
34
|
+
const profileStart = profile ? process.hrtime.bigint() : undefined;
|
|
35
|
+
let familyFlagMs = 0;
|
|
36
|
+
let rootResolutionMs = 0;
|
|
37
|
+
let handlerImportMs = 0;
|
|
38
|
+
let stdinReadMs = 0;
|
|
39
|
+
const profileMark = () => profile ? process.hrtime.bigint() : undefined;
|
|
40
|
+
const profileElapsed = (start, end) => start === undefined || end === undefined ? 0 : Number(end - start) / 1_000_000;
|
|
26
41
|
const entry = parsed.entry;
|
|
27
42
|
// The full graph's process gate accepts textual true/false overrides. It ignores
|
|
28
43
|
// numeric env values, so preserve that behavior for family flags here too.
|
|
@@ -36,6 +51,7 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
|
36
51
|
await runCli();
|
|
37
52
|
return;
|
|
38
53
|
}
|
|
54
|
+
familyFlagMs = profileElapsed(profileStart, profileMark());
|
|
39
55
|
const useShell = entry.kind === "native" && Boolean(entry.familyFlagKey) && !familyEnabled;
|
|
40
56
|
let worktreeScopeFixEnabled;
|
|
41
57
|
if (entry.name === "repo-run-registry") {
|
|
@@ -53,21 +69,32 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
|
53
69
|
// fail-soft behavior; newly routed commands fall back to the full graph so
|
|
54
70
|
// its expected-user-error formatting remains authoritative.
|
|
55
71
|
let hqRoot;
|
|
72
|
+
const rootResolutionStartedAt = profileMark();
|
|
56
73
|
if (shouldInjectRoot) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
const shortcutRoot = entry.name === "checkpoint-stop-gate" || entry.name === "session-journal"
|
|
75
|
+
? resolveExplicitFastLiveRoot(parsed.hqRootArg, process.env)
|
|
76
|
+
: undefined;
|
|
77
|
+
if (shortcutRoot !== undefined) {
|
|
78
|
+
hqRoot = shortcutRoot;
|
|
60
79
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
80
|
+
else {
|
|
81
|
+
const { resolveLiveRoot } = await import("./utils/hq-roots-core.js");
|
|
82
|
+
try {
|
|
83
|
+
hqRoot = resolveLiveRoot(parsed.hqRootArg ? { hqRoot: parsed.hqRootArg } : {});
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
if (entry.kind === "native") {
|
|
87
|
+
const { runCli } = await import("./main.js");
|
|
88
|
+
await runCli();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
hqRoot = undefined;
|
|
66
92
|
}
|
|
67
|
-
hqRoot = undefined;
|
|
68
93
|
}
|
|
69
94
|
}
|
|
95
|
+
rootResolutionMs = profileElapsed(rootResolutionStartedAt, profileMark());
|
|
70
96
|
if (entry.kind === "native" && !useShell) {
|
|
97
|
+
const handlerImportStartedAt = profileMark();
|
|
71
98
|
const { installFastPathStreamErrorHandlers } = await import("./utils/fast-stream-errors.js");
|
|
72
99
|
installFastPathStreamErrorHandlers();
|
|
73
100
|
const handlerModule = await import(entry.modulePath);
|
|
@@ -75,8 +102,21 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
|
75
102
|
if (typeof handler !== "function") {
|
|
76
103
|
throw new Error(`Native fast handler ${entry.exportName} was not exported`);
|
|
77
104
|
}
|
|
105
|
+
handlerImportMs = profileElapsed(handlerImportStartedAt, profileMark());
|
|
106
|
+
const stdinStartedAt = profileMark();
|
|
78
107
|
const readsStdin = entry.readsStdinFor?.(parsed.args) ?? entry.readsStdin ?? false;
|
|
79
108
|
const stdin = readsStdin ? await readStdinIfPiped(process.stdin) : "";
|
|
109
|
+
stdinReadMs = profileElapsed(stdinStartedAt, profileMark());
|
|
110
|
+
if (profile && profileStart !== undefined) {
|
|
111
|
+
process.stderr.write(`HQ_FAST_ROUTE_PROFILE ${JSON.stringify({
|
|
112
|
+
command: entry.name,
|
|
113
|
+
pre_handler_ms: profileElapsed(profileStart, profileMark()),
|
|
114
|
+
family_flag_ms: familyFlagMs,
|
|
115
|
+
root_resolution_ms: rootResolutionMs,
|
|
116
|
+
handler_import_ms: handlerImportMs,
|
|
117
|
+
stdin_read_ms: stdinReadMs,
|
|
118
|
+
})}\n`);
|
|
119
|
+
}
|
|
80
120
|
const code = await handler(parsed.args, stdin, process.cwd(), hqRoot, entry.name.split(" ").at(-1) ?? entry.name, parsed.hqRootArg, worktreeScopeFixEnabled);
|
|
81
121
|
if (code === undefined) {
|
|
82
122
|
if (!entry.fallbackToCommandGraph) {
|
|
@@ -120,6 +160,29 @@ export async function runFastCore(argv, parsed = parseFastCore(argv)) {
|
|
|
120
160
|
// Propagate verbatim: these scripts use their exit code as their interface.
|
|
121
161
|
process.exit(code);
|
|
122
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Resolve valid explicit roots for the two measured hook routes without loading
|
|
165
|
+
* the general root resolver. Anything uncertain falls through to that resolver,
|
|
166
|
+
* which retains its diagnostics and cwd-discovery behavior.
|
|
167
|
+
*/
|
|
168
|
+
export function resolveExplicitFastLiveRoot(hqRootArg, env) {
|
|
169
|
+
const candidate = hqRootArg || env.HQ_ROOT || env.CLAUDE_PROJECT_DIR;
|
|
170
|
+
if (!candidate)
|
|
171
|
+
return undefined;
|
|
172
|
+
try {
|
|
173
|
+
const resolved = fs.realpathSync(candidate);
|
|
174
|
+
const packageRoot = fs.realpathSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
175
|
+
const relative = path.relative(packageRoot, resolved);
|
|
176
|
+
if (relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)))
|
|
177
|
+
return undefined;
|
|
178
|
+
if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory())
|
|
179
|
+
return undefined;
|
|
180
|
+
return resolved;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
123
186
|
/** Match hq-flags-client's literal true/false env override without loading its module. */
|
|
124
187
|
function localBooleanFlagOverride(flagKey, env) {
|
|
125
188
|
const envKey = `HQ_FLAG_${flagKey.replace(/[.-]/g, "_").toUpperCase()}`;
|