@indigoai-us/hq-cli 5.318.1-beta.0 → 5.320.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 +10 -2
- package/assets/scaffold/core/scripts/repo-run-registry.sh +61 -17
- package/assets/scaffold/core/scripts/session-journal.sh +40 -2
- package/dist/bin/hq-auth-refresh.d.ts +3 -1
- package/dist/bin/hq-auth-refresh.js +41 -6
- package/dist/command-catalog.generated.d.ts +22 -0
- package/dist/command-catalog.generated.js +29 -0
- package/dist/commands/cloud.js +0 -1
- package/dist/commands/core-hook-state-helpers-native.d.ts +1 -1
- package/dist/commands/core-hook-state-helpers-native.js +36 -7
- package/dist/commands/core-repo-run-registry-native.d.ts +1 -0
- package/dist/commands/core-repo-run-registry-native.js +10 -2
- package/dist/commands/core-rrr-worktree-scope-fix-flag.d.ts +4 -0
- package/dist/commands/core-rrr-worktree-scope-fix-flag.js +7 -0
- package/dist/commands/core-session-journal-native.js +31 -2
- package/dist/commands/lanes-checks.js +12 -0
- package/dist/commands/lanes-dropbox-repair.d.ts +14 -0
- package/dist/commands/lanes-dropbox-repair.js +33 -0
- package/dist/commands/lanes.js +44 -5
- package/dist/fast-core.js +6 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.js +135 -63
- package/dist/lib/daemon/run.d.ts +2 -0
- package/dist/lib/daemon/run.js +10 -1
- package/dist/lib/lanes/autopilot.js +9 -2
- package/dist/lib/lanes/flag-keys.d.ts +9 -0
- package/dist/lib/lanes/flag-keys.js +9 -0
- package/dist/lib/lanes/keep-alive-audit.d.ts +1 -1
- package/dist/lib/lanes/keep-alive-audit.js +69 -4
- package/dist/lib/lanes/orphaned-dropbox.d.ts +24 -0
- package/dist/lib/lanes/orphaned-dropbox.js +213 -0
- package/dist/lib/lanes/tracker-checks.d.ts +1 -1
- package/dist/utils/package-use-lease.d.ts +25 -0
- package/dist/utils/package-use-lease.js +213 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
-
## [5.
|
|
5
|
+
## [5.320.0] — 2026-10-02
|
|
6
|
+
- Write session-journal titles as escaped YAML strings so free-form titles remain parseable.
|
|
7
|
+
- Added a default-off hq-flags gate that lets lane list pollers skip remote owner refreshes to reduce poll latency.
|
|
8
|
+
- Publish per-process package-use leases so Desktop updates can wait for active hq-cli work without native locks.
|
|
9
|
+
|
|
10
|
+
## [5.319.0] — 2026-10-02
|
|
6
11
|
- Let `hq access` use ACL-checked company presigned reads when STS vend rejects a truncated write-policy scope.
|
|
7
|
-
-
|
|
12
|
+
- Lead autopilot on an Outpost can wake leads that were created on that Outpost, behind the default-off `lanes.autopilot-outpost-native-leads` flag. Before, those leads were skipped as a host mismatch and got no wakes.
|
|
13
|
+
- Add a default-off fix for repo-run-registry worktree-scoped owner matching.
|
|
14
|
+
- Correct keep-alive audit verdicts for pending-work wakes and lanes that were not opted in.
|
|
15
|
+
- Detect and safely redeliver messages stranded in bare-ULID lane dropboxes.
|
|
8
16
|
|
|
9
17
|
## [5.318.0] — 2026-10-02
|
|
10
18
|
- `hq lanes autopilot run` now marks missing lead registry entries as stale and includes a command to remove each stale row.
|
|
@@ -330,12 +330,32 @@ _cmd_owner_of() {
|
|
|
330
330
|
[[ -z "$repo" ]] && { echo ""; return; }
|
|
331
331
|
_ensure_reg
|
|
332
332
|
_prune_stale_locked
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
333
|
+
# hq core sets this bridge from the cached hq-flags value; absent means off.
|
|
334
|
+
if [[ "${HQ_FLAG_CORE_RRR_WORKTREE_SCOPE_FIX:-false}" == "true" ]]; then
|
|
335
|
+
jq --arg repo "$repo" --arg target "$(_abs_path "$path")" \
|
|
336
|
+
'def worktree_root($scope):
|
|
337
|
+
($scope | sub("^worktree:"; "")) as $raw
|
|
338
|
+
| ($raw | sub("/+$"; "")) as $trimmed
|
|
339
|
+
| if $trimmed == "" and ($raw | startswith("/")) then "/" else $trimmed end;
|
|
340
|
+
def worktree_covers($target; $scope):
|
|
341
|
+
worktree_root($scope) as $root
|
|
342
|
+
| if $root == "/" then ($target | startswith("/"))
|
|
343
|
+
elif $root == "" then false
|
|
344
|
+
else ($target == $root or ($target | startswith($root + "/")))
|
|
345
|
+
end;
|
|
346
|
+
[.runs[] as $entry | select(
|
|
347
|
+
($entry.scope == "repo" and $entry.repo_path == $repo)
|
|
348
|
+
or (($entry.scope | startswith("worktree:")) and
|
|
349
|
+
worktree_covers($target; $entry.scope))
|
|
350
|
+
) | $entry]' "$REG_FILE"
|
|
351
|
+
else
|
|
352
|
+
jq --arg repo "$repo" --arg target "$(_abs_path "$path")" \
|
|
353
|
+
'[.runs[] | select(
|
|
354
|
+
(.scope == "repo" and .repo_path == $repo)
|
|
355
|
+
or (.scope | startswith("worktree:")) and
|
|
356
|
+
($target | startswith(.scope | sub("^worktree:"; "")))
|
|
357
|
+
)]' "$REG_FILE"
|
|
358
|
+
fi
|
|
339
359
|
}
|
|
340
360
|
|
|
341
361
|
# Check if a target path is owned by a foreign run.
|
|
@@ -363,17 +383,41 @@ _cmd_check() {
|
|
|
363
383
|
|
|
364
384
|
# Collect foreign entries covering this target
|
|
365
385
|
local matches
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
386
|
+
if [[ "${HQ_FLAG_CORE_RRR_WORKTREE_SCOPE_FIX:-false}" == "true" ]]; then
|
|
387
|
+
matches=$(jq -c --arg repo "$repo" \
|
|
388
|
+
--arg target "$abs_target" \
|
|
389
|
+
--arg mypid "${my_pid:-0}" \
|
|
390
|
+
--arg mysid "${my_session:-__none__}" \
|
|
391
|
+
'def worktree_root($scope):
|
|
392
|
+
($scope | sub("^worktree:"; "")) as $raw
|
|
393
|
+
| ($raw | sub("/+$"; "")) as $trimmed
|
|
394
|
+
| if $trimmed == "" and ($raw | startswith("/")) then "/" else $trimmed end;
|
|
395
|
+
def worktree_covers($target; $scope):
|
|
396
|
+
worktree_root($scope) as $root
|
|
397
|
+
| if $root == "/" then ($target | startswith("/"))
|
|
398
|
+
elif $root == "" then false
|
|
399
|
+
else ($target == $root or ($target | startswith($root + "/")))
|
|
400
|
+
end;
|
|
401
|
+
[.runs[] as $entry | select(
|
|
402
|
+
(($entry.scope == "repo" and $entry.repo_path == $repo)
|
|
403
|
+
or (($entry.scope | startswith("worktree:")) and
|
|
404
|
+
worktree_covers($target; $entry.scope)))
|
|
405
|
+
and (($entry.pid | tostring) != $mypid)
|
|
406
|
+
and ($entry.session_id != $mysid)
|
|
407
|
+
) | $entry]' "$REG_FILE" 2>/dev/null || echo '[]')
|
|
408
|
+
else
|
|
409
|
+
matches=$(jq -c --arg repo "$repo" \
|
|
410
|
+
--arg target "$abs_target" \
|
|
411
|
+
--arg mypid "${my_pid:-0}" \
|
|
412
|
+
--arg mysid "${my_session:-__none__}" \
|
|
413
|
+
'[.runs[] | select(
|
|
414
|
+
((.scope == "repo" and .repo_path == $repo)
|
|
415
|
+
or ((.scope | startswith("worktree:")) and
|
|
416
|
+
($target | startswith(.scope | sub("^worktree:"; "")))))
|
|
417
|
+
and ((.pid | tostring) != $mypid)
|
|
418
|
+
and (.session_id != $mysid)
|
|
419
|
+
)]' "$REG_FILE" 2>/dev/null || echo '[]')
|
|
420
|
+
fi
|
|
377
421
|
|
|
378
422
|
local n
|
|
379
423
|
n=$(echo "$matches" | jq 'length')
|
|
@@ -43,6 +43,41 @@ slugify() {
|
|
|
43
43
|
| cut -c1-40
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
yaml_double_quoted() {
|
|
47
|
+
local value="$1" output='"' character next codepoint hex
|
|
48
|
+
local LC_ALL=C index
|
|
49
|
+
for ((index = 0; index < ${#value}; index++)); do
|
|
50
|
+
character="${value:index:1}"
|
|
51
|
+
if [[ "$character" == $'\xc2' ]] && (( index + 1 < ${#value} )); then
|
|
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"
|
|
56
|
+
output+="\\u$hex"
|
|
57
|
+
index=$((index + 1))
|
|
58
|
+
continue
|
|
59
|
+
fi
|
|
60
|
+
fi
|
|
61
|
+
case "$character" in
|
|
62
|
+
\\) output+='\\' ;;
|
|
63
|
+
'"') output+='\"' ;;
|
|
64
|
+
$'\n') output+='\n' ;;
|
|
65
|
+
$'\r') output+='\r' ;;
|
|
66
|
+
$'\t') output+='\t' ;;
|
|
67
|
+
*)
|
|
68
|
+
if [[ "$character" == [[:cntrl:]] ]]; then
|
|
69
|
+
printf -v codepoint '%d' "'${character}"
|
|
70
|
+
printf -v hex '%02x' "$codepoint"
|
|
71
|
+
output+="\\x$hex"
|
|
72
|
+
else
|
|
73
|
+
output+="$character"
|
|
74
|
+
fi
|
|
75
|
+
;;
|
|
76
|
+
esac
|
|
77
|
+
done
|
|
78
|
+
printf '%s"' "$output"
|
|
79
|
+
}
|
|
80
|
+
|
|
46
81
|
# Counters are per-session. A date-global counter lets concurrent Claude and
|
|
47
82
|
# Codex tasks trip (and reset) each other's journal reminders.
|
|
48
83
|
counter_key() {
|
|
@@ -140,6 +175,9 @@ case "$cmd" in
|
|
|
140
175
|
|
|
141
176
|
slug="$(slugify "$title")"
|
|
142
177
|
[ -z "$slug" ] && slug="entry"
|
|
178
|
+
yaml_title="$(yaml_double_quoted "$title")"
|
|
179
|
+
yaml_slug="$slug"
|
|
180
|
+
[[ "$slug" != *$'\n'* ]] || yaml_slug="$(yaml_double_quoted "$slug")"
|
|
143
181
|
JOURNAL_WRITE_LOCK=""
|
|
144
182
|
acquire_journal_write_lock "$dir" || { warn "write: failed to acquire journal lock"; exit 0; }
|
|
145
183
|
trap release_journal_write_lock EXIT
|
|
@@ -161,8 +199,8 @@ case "$cmd" in
|
|
|
161
199
|
{
|
|
162
200
|
printf -- '---\n'
|
|
163
201
|
printf 'ts: %s\n' "$(now_iso_z)"
|
|
164
|
-
printf 'title: %s\n' "$
|
|
165
|
-
printf 'slug: %s\n' "$
|
|
202
|
+
printf 'title: %s\n' "$yaml_title"
|
|
203
|
+
printf 'slug: %s\n' "$yaml_slug"
|
|
166
204
|
if [ -n "$files_csv" ]; then
|
|
167
205
|
printf 'files:\n'
|
|
168
206
|
old_ifs="$IFS"
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* non-interactively.
|
|
12
12
|
*/
|
|
13
13
|
import "../node-preflight.js";
|
|
14
|
+
import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
|
|
14
15
|
import "../node-network-compat.js";
|
|
15
|
-
import { refreshCachedSession } from "../utils/cognito-session.js";
|
|
16
|
+
import type { refreshCachedSession } from "../utils/cognito-session.js";
|
|
16
17
|
export declare function runAuthRefresh(refreshSession?: typeof refreshCachedSession): Promise<number>;
|
|
18
|
+
export declare function startAuthRefresh(acquireLease?: typeof acquireCurrentPackageUseLease): Promise<number>;
|
|
17
19
|
//# sourceMappingURL=hq-auth-refresh.d.ts.map
|
|
@@ -13,15 +13,16 @@
|
|
|
13
13
|
// MUST be first: guard the Node version before any dependency that needs a
|
|
14
14
|
// Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
|
|
15
15
|
import "../node-preflight.js";
|
|
16
|
+
import { acquireCurrentPackageUseLease } from "../utils/package-use-lease.js";
|
|
16
17
|
import "../node-network-compat.js";
|
|
17
18
|
import { fileURLToPath } from "node:url";
|
|
18
19
|
import path from "node:path";
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
export async function runAuthRefresh(refreshSession) {
|
|
21
|
+
const { finishSentrySession, Sentry } = await import("../sentry.js");
|
|
22
|
+
const { refreshCachedSession: refresh } = await import("../utils/cognito-session.js");
|
|
22
23
|
let exitCode = 1;
|
|
23
24
|
try {
|
|
24
|
-
const result = await refreshSession();
|
|
25
|
+
const result = await (refreshSession ?? refresh)();
|
|
25
26
|
if (result.refreshed) {
|
|
26
27
|
exitCode = 0;
|
|
27
28
|
}
|
|
@@ -41,8 +42,42 @@ export async function runAuthRefresh(refreshSession = refreshCachedSession) {
|
|
|
41
42
|
}
|
|
42
43
|
return exitCode;
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
function isPackageUseUpdatePendingError(error) {
|
|
46
|
+
return Boolean(error &&
|
|
47
|
+
typeof error === "object" &&
|
|
48
|
+
error.name === "PackageUseUpdatePendingError");
|
|
49
|
+
}
|
|
50
|
+
function packageUseErrorClass(error) {
|
|
51
|
+
if (!error || typeof error !== "object")
|
|
52
|
+
return "unknown";
|
|
53
|
+
const value = error;
|
|
54
|
+
if (typeof value.code === "string")
|
|
55
|
+
return value.code;
|
|
56
|
+
if (value.cause && typeof value.cause === "object") {
|
|
57
|
+
const cause = value.cause;
|
|
58
|
+
if (typeof cause.code === "string")
|
|
59
|
+
return cause.code;
|
|
60
|
+
if (typeof cause.name === "string")
|
|
61
|
+
return cause.name;
|
|
62
|
+
}
|
|
63
|
+
return typeof value.name === "string" ? value.name : "unknown";
|
|
64
|
+
}
|
|
65
|
+
export async function startAuthRefresh(acquireLease = acquireCurrentPackageUseLease) {
|
|
66
|
+
try {
|
|
67
|
+
acquireLease();
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (isPackageUseUpdatePendingError(error)) {
|
|
71
|
+
process.stderr.write(`hq-auth-refresh: ${error instanceof Error ? error.message : "The HQ CLI package is being updated"}\n`);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
process.stderr.write(`hq-auth-refresh: package-use lease unavailable (${packageUseErrorClass(error)}); continuing without lease\n`);
|
|
75
|
+
}
|
|
76
|
+
const { initSentry } = await import("../sentry.js");
|
|
45
77
|
initSentry();
|
|
46
|
-
|
|
78
|
+
return runAuthRefresh();
|
|
79
|
+
}
|
|
80
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
81
|
+
void startAuthRefresh().then((exitCode) => process.exit(exitCode));
|
|
47
82
|
}
|
|
48
83
|
//# sourceMappingURL=hq-auth-refresh.js.map
|
|
@@ -7841,6 +7841,28 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
7841
7841
|
readonly description: "Print a human-readable table";
|
|
7842
7842
|
}];
|
|
7843
7843
|
readonly subcommands: readonly [];
|
|
7844
|
+
}, {
|
|
7845
|
+
readonly name: "repair-dropbox";
|
|
7846
|
+
readonly description: "Inspect or redeliver messages stranded in a bare-ULID lane dropbox";
|
|
7847
|
+
readonly aliases: readonly [];
|
|
7848
|
+
readonly hidden: false;
|
|
7849
|
+
readonly usage: "[options] <ulid>";
|
|
7850
|
+
readonly arguments: readonly [{
|
|
7851
|
+
readonly name: "ulid";
|
|
7852
|
+
readonly required: true;
|
|
7853
|
+
readonly variadic: false;
|
|
7854
|
+
}];
|
|
7855
|
+
readonly options: readonly [{
|
|
7856
|
+
readonly flags: "--company <slug>";
|
|
7857
|
+
readonly description: "Select the company by slug";
|
|
7858
|
+
}, {
|
|
7859
|
+
readonly flags: "--apply";
|
|
7860
|
+
readonly description: "Append marked messages to the canonical lane inbox and archive the orphan folder";
|
|
7861
|
+
}, {
|
|
7862
|
+
readonly flags: "--json";
|
|
7863
|
+
readonly description: "JSON on stdout (always)";
|
|
7864
|
+
}];
|
|
7865
|
+
readonly subcommands: readonly [];
|
|
7844
7866
|
}, {
|
|
7845
7867
|
readonly name: "report";
|
|
7846
7868
|
readonly description: "Generate the owner report for tracker teams";
|
|
@@ -10087,6 +10087,35 @@ export const COMMAND_CATALOG = [
|
|
|
10087
10087
|
],
|
|
10088
10088
|
"subcommands": []
|
|
10089
10089
|
},
|
|
10090
|
+
{
|
|
10091
|
+
"name": "repair-dropbox",
|
|
10092
|
+
"description": "Inspect or redeliver messages stranded in a bare-ULID lane dropbox",
|
|
10093
|
+
"aliases": [],
|
|
10094
|
+
"hidden": false,
|
|
10095
|
+
"usage": "[options] <ulid>",
|
|
10096
|
+
"arguments": [
|
|
10097
|
+
{
|
|
10098
|
+
"name": "ulid",
|
|
10099
|
+
"required": true,
|
|
10100
|
+
"variadic": false
|
|
10101
|
+
}
|
|
10102
|
+
],
|
|
10103
|
+
"options": [
|
|
10104
|
+
{
|
|
10105
|
+
"flags": "--company <slug>",
|
|
10106
|
+
"description": "Select the company by slug"
|
|
10107
|
+
},
|
|
10108
|
+
{
|
|
10109
|
+
"flags": "--apply",
|
|
10110
|
+
"description": "Append marked messages to the canonical lane inbox and archive the orphan folder"
|
|
10111
|
+
},
|
|
10112
|
+
{
|
|
10113
|
+
"flags": "--json",
|
|
10114
|
+
"description": "JSON on stdout (always)"
|
|
10115
|
+
}
|
|
10116
|
+
],
|
|
10117
|
+
"subcommands": []
|
|
10118
|
+
},
|
|
10090
10119
|
{
|
|
10091
10120
|
"name": "report",
|
|
10092
10121
|
"description": "Generate the owner report for tracker teams",
|
package/dist/commands/cloud.js
CHANGED
|
@@ -1697,7 +1697,6 @@ async function runNowSingle(hqRoot, company, personal, message, onConflict, mode
|
|
|
1697
1697
|
hqRoot,
|
|
1698
1698
|
progressRecorder: trackingProgressRecorder(planLimitTracker, pushProgressCompany),
|
|
1699
1699
|
skipUnchanged: true,
|
|
1700
|
-
serverOwnedPathPolicy: "skip",
|
|
1701
1700
|
propagateDeletes: true,
|
|
1702
1701
|
propagateDeletePolicy: resolveDeletePolicy(),
|
|
1703
1702
|
...(onConflict ? { onConflict } : {}),
|
|
@@ -6,6 +6,6 @@ import { runDetectStaleReviewBase } from "./core-detect-stale-review-base-native
|
|
|
6
6
|
export { runShareSuggestionState, runRepoRunRegistry, runDetectStaleReviewBase };
|
|
7
7
|
export declare function registerCoreHookStateHelpers(core: Command): void;
|
|
8
8
|
export declare function runShareSuggestionStateFast(args: readonly string[], stdin: string, _cwd: string, hqRoot: string | undefined): number;
|
|
9
|
-
export declare function runRepoRunRegistryFast(args: readonly string[], _stdin: string, _cwd: string, hqRoot: string | undefined): number;
|
|
9
|
+
export declare function runRepoRunRegistryFast(args: readonly string[], _stdin: string, _cwd: string, hqRoot: string | undefined, _commandName: string, _hqRootArg?: string, worktreeScopeFixEnabled?: boolean): number;
|
|
10
10
|
export declare function runDetectStaleReviewBaseFast(args: readonly string[], _stdin: string, _cwd: string, hqRoot: string | undefined): number;
|
|
11
11
|
//# sourceMappingURL=core-hook-state-helpers-native.d.ts.map
|
|
@@ -2,16 +2,28 @@ import { resolveBundledAsset, resolveLiveRoot } from "../utils/hq-roots.js";
|
|
|
2
2
|
import { runBundledScript } from "../utils/run-bundled-script.js";
|
|
3
3
|
import { describeCoreCommand } from "./core-command-catalog.js";
|
|
4
4
|
import { coreNativeHookHelpersEnabled } from "./core-native-hook-helpers-flag.js";
|
|
5
|
+
import { coreRrrWorktreeScopeFixEnabled } from "./core-rrr-worktree-scope-fix-flag.js";
|
|
5
6
|
import { runShareSuggestionState, SHARE_SUGGESTION_STATE_ASSET } from "./core-share-suggestion-state-native.js";
|
|
6
7
|
import { runRepoRunRegistry, REPO_RUN_REGISTRY_ASSET } from "./core-repo-run-registry-native.js";
|
|
7
8
|
import { runDetectStaleReviewBase, DETECT_STALE_REVIEW_BASE_ASSET } from "./core-detect-stale-review-base-native.js";
|
|
8
9
|
export { runShareSuggestionState, runRepoRunRegistry, runDetectStaleReviewBase };
|
|
9
10
|
const definitions = [
|
|
10
11
|
{ name: "share-suggestion-state", asset: SHARE_SUGGESTION_STATE_ASSET, summary: "Manage hook share suggestion state", run: (args, stdin, _cwd, root) => runShareSuggestionState(args, stdin, { root, cwd: root, env: process.env, stdinIsTty: process.stdin.isTTY }) },
|
|
11
|
-
{
|
|
12
|
+
{
|
|
13
|
+
name: "repo-run-registry",
|
|
14
|
+
asset: REPO_RUN_REGISTRY_ASSET,
|
|
15
|
+
summary: "Coordinate active runs across repositories",
|
|
16
|
+
run: (args, _stdin, _cwd, root, worktreeScopeFixEnabled) => runRepoRunRegistry(args, {
|
|
17
|
+
root,
|
|
18
|
+
cwd: root,
|
|
19
|
+
env: process.env,
|
|
20
|
+
worktreeScopeFixEnabled,
|
|
21
|
+
runShellFallback: (fallbackArgs) => {
|
|
12
22
|
const { code } = runBundledScript({ asset: REPO_RUN_REGISTRY_ASSET, args: [...fallbackArgs], cwd: root, hqRoot: root });
|
|
13
23
|
return { code, stdout: "", stderr: "" };
|
|
14
|
-
}
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
},
|
|
15
27
|
{ name: "detect-stale-review-base", asset: DETECT_STALE_REVIEW_BASE_ASSET, summary: "Find stale remote review bases in an HQ root", run: (args, _stdin, _cwd, root) => runDetectStaleReviewBase(args, { cwd: root, env: { ...process.env, HQ_ROOT: root }, scriptPath: resolveBundledAsset(DETECT_STALE_REVIEW_BASE_ASSET) }) },
|
|
16
28
|
];
|
|
17
29
|
async function readStdin() {
|
|
@@ -37,14 +49,25 @@ export function registerCoreHookStateHelpers(core) {
|
|
|
37
49
|
const cwd = process.cwd(), hqRootArg = core.opts().hqRoot;
|
|
38
50
|
const root = resolveLiveRoot(hqRootArg ? { hqRoot: hqRootArg } : {});
|
|
39
51
|
const operands = cmd.args.length > 0 ? cmd.args : args;
|
|
52
|
+
const worktreeScopeFixEnabled = entry.name === "repo-run-registry"
|
|
53
|
+
? await coreRrrWorktreeScopeFixEnabled()
|
|
54
|
+
: undefined;
|
|
40
55
|
if (!await coreNativeHookHelpersEnabled()) {
|
|
41
|
-
const { code } = runBundledScript({
|
|
56
|
+
const { code } = runBundledScript({
|
|
57
|
+
asset: entry.asset,
|
|
58
|
+
args: operands,
|
|
59
|
+
cwd: root,
|
|
60
|
+
hqRoot: root,
|
|
61
|
+
...(entry.name === "repo-run-registry"
|
|
62
|
+
? { env: { HQ_FLAG_CORE_RRR_WORKTREE_SCOPE_FIX: String(worktreeScopeFixEnabled) } }
|
|
63
|
+
: {}),
|
|
64
|
+
});
|
|
42
65
|
if (code !== 0)
|
|
43
66
|
process.exitCode = code;
|
|
44
67
|
return;
|
|
45
68
|
}
|
|
46
69
|
const stdin = entry.name === "share-suggestion-state" ? await readStdin() : "";
|
|
47
|
-
writeResult(await entry.run(operands, stdin, cwd, root));
|
|
70
|
+
writeResult(await entry.run(operands, stdin, cwd, root, worktreeScopeFixEnabled));
|
|
48
71
|
});
|
|
49
72
|
describeCoreCommand(command, { execution: "native", asset: entry.asset, root: "live" });
|
|
50
73
|
}
|
|
@@ -60,12 +83,18 @@ export function runShareSuggestionStateFast(args, stdin, _cwd, hqRoot) {
|
|
|
60
83
|
const root = hqRoot ?? resolveLiveRoot();
|
|
61
84
|
return writeFast(runShareSuggestionState(args, stdin, { root, cwd: root, env: process.env, stdinIsTty: process.stdin.isTTY }));
|
|
62
85
|
}
|
|
63
|
-
export function runRepoRunRegistryFast(args, _stdin, _cwd, hqRoot) {
|
|
86
|
+
export function runRepoRunRegistryFast(args, _stdin, _cwd, hqRoot, _commandName, _hqRootArg, worktreeScopeFixEnabled = false) {
|
|
64
87
|
const root = hqRoot ?? resolveLiveRoot();
|
|
65
|
-
return writeFast(runRepoRunRegistry(args, {
|
|
88
|
+
return writeFast(runRepoRunRegistry(args, {
|
|
89
|
+
root,
|
|
90
|
+
cwd: root,
|
|
91
|
+
env: process.env,
|
|
92
|
+
worktreeScopeFixEnabled,
|
|
93
|
+
runShellFallback: (fallbackArgs) => {
|
|
66
94
|
const { code } = runBundledScript({ asset: REPO_RUN_REGISTRY_ASSET, args: [...fallbackArgs], cwd: root, hqRoot: root });
|
|
67
95
|
return { code, stdout: "", stderr: "" };
|
|
68
|
-
}
|
|
96
|
+
},
|
|
97
|
+
}));
|
|
69
98
|
}
|
|
70
99
|
export function runDetectStaleReviewBaseFast(args, _stdin, _cwd, hqRoot) {
|
|
71
100
|
const root = hqRoot ?? resolveLiveRoot();
|
|
@@ -13,6 +13,7 @@ export type RegistryDependencies = {
|
|
|
13
13
|
hostname?: () => string;
|
|
14
14
|
sleep?: (milliseconds: number) => void;
|
|
15
15
|
parseDateEpoch?: (value: string) => number;
|
|
16
|
+
worktreeScopeFixEnabled?: boolean;
|
|
16
17
|
runShellFallback?: (args: readonly string[]) => HookHelperOutput;
|
|
17
18
|
};
|
|
18
19
|
export declare function runRepoRunRegistry(args: readonly string[], deps: RegistryDependencies): HookHelperOutput;
|
|
@@ -40,6 +40,14 @@ function pathAbs(value, cwd, f) {
|
|
|
40
40
|
return value;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
function worktreeScopeCovers(target, scope) {
|
|
44
|
+
const rawRoot = scope.slice("worktree:".length);
|
|
45
|
+
const trimmedRoot = rawRoot.replace(/\/+$/, "");
|
|
46
|
+
const root = trimmedRoot === "" && rawRoot.startsWith("/") ? "/" : trimmedRoot;
|
|
47
|
+
if (root === "/")
|
|
48
|
+
return target.startsWith("/");
|
|
49
|
+
return root !== "" && (target === root || target.startsWith(`${root}/`));
|
|
50
|
+
}
|
|
43
51
|
function parseStaleMinutes(root, f) {
|
|
44
52
|
const personal = path.join(root, "personal/settings/orchestrator.yaml");
|
|
45
53
|
const core = path.join(root, "core/settings/orchestrator.yaml");
|
|
@@ -215,7 +223,7 @@ export function runRepoRunRegistry(args, deps) {
|
|
|
215
223
|
const state = JSON.parse(f.readFileSync(regFile, "utf8"));
|
|
216
224
|
if (!state || typeof state !== "object" || !Array.isArray(state.runs))
|
|
217
225
|
return deps.runShellFallback(args);
|
|
218
|
-
if ((sub === "owner-of" || sub === "check") && state.runs.some((run) => typeof run?.scope === "string" && run.scope.startsWith("worktree:"))) {
|
|
226
|
+
if (!deps.worktreeScopeFixEnabled && (sub === "owner-of" || sub === "check") && state.runs.some((run) => typeof run?.scope === "string" && run.scope.startsWith("worktree:"))) {
|
|
219
227
|
return deps.runShellFallback(args);
|
|
220
228
|
}
|
|
221
229
|
}
|
|
@@ -338,7 +346,7 @@ export function runRepoRunRegistry(args, deps) {
|
|
|
338
346
|
return output();
|
|
339
347
|
const absoluteTarget = pathAbs(target, deps.cwd, f), myPid = valueAfter(rest, "--pid") || "0", mySession = valueAfter(rest, "--session-id") || "__none__";
|
|
340
348
|
const runs = Array.isArray(state.runs) ? state.runs : [];
|
|
341
|
-
const covers = (run) => (run?.scope === "repo" && run?.repo_path === repo) || (typeof run?.scope === "string" && run.scope.startsWith("worktree:") && absoluteTarget
|
|
349
|
+
const covers = (run) => (run?.scope === "repo" && run?.repo_path === repo) || (typeof run?.scope === "string" && run.scope.startsWith("worktree:") && worktreeScopeCovers(absoluteTarget, run.scope));
|
|
342
350
|
if (sub === "owner-of")
|
|
343
351
|
return jsonOutput(0, runs.filter(covers));
|
|
344
352
|
const owners = runs.filter((run) => covers(run) && String(run?.pid) !== myPid && run?.session_id !== mySession);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Default-off rollout gate for repo-run-registry worktree-scope matching. */
|
|
2
|
+
export declare const CORE_RRR_WORKTREE_SCOPE_FIX_FLAG = "core-rrr-worktree-scope-fix";
|
|
3
|
+
export declare function coreRrrWorktreeScopeFixEnabled(): Promise<boolean>;
|
|
4
|
+
//# sourceMappingURL=core-rrr-worktree-scope-fix-flag.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Default-off rollout gate for repo-run-registry worktree-scope matching. */
|
|
2
|
+
export const CORE_RRR_WORKTREE_SCOPE_FIX_FLAG = "core-rrr-worktree-scope-fix";
|
|
3
|
+
export async function coreRrrWorktreeScopeFixEnabled() {
|
|
4
|
+
const { resolveProcessFlagGate } = await import("../lib/flag-registry.js");
|
|
5
|
+
return resolveProcessFlagGate(CORE_RRR_WORKTREE_SCOPE_FIX_FLAG, { fallback: false }, () => false);
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=core-rrr-worktree-scope-fix-flag.js.map
|
|
@@ -24,7 +24,35 @@ const counterKey = (value) => {
|
|
|
24
24
|
return key.length > 96 ? key.slice(0, 96) : key;
|
|
25
25
|
};
|
|
26
26
|
const slugify = (value) => value.toLowerCase()
|
|
27
|
-
.
|
|
27
|
+
.split("\n")
|
|
28
|
+
.map((line) => line.replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "").slice(0, 40))
|
|
29
|
+
.join("\n");
|
|
30
|
+
function yamlDoubleQuoted(value) {
|
|
31
|
+
let escaped = "";
|
|
32
|
+
for (const character of value) {
|
|
33
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
34
|
+
if (character === "\\")
|
|
35
|
+
escaped += "\\\\";
|
|
36
|
+
else if (character === '"')
|
|
37
|
+
escaped += '\\"';
|
|
38
|
+
else if (character === "\n")
|
|
39
|
+
escaped += "\\n";
|
|
40
|
+
else if (character === "\r")
|
|
41
|
+
escaped += "\\r";
|
|
42
|
+
else if (character === "\t")
|
|
43
|
+
escaped += "\\t";
|
|
44
|
+
else if (codePoint < 0x20 || codePoint === 0x7f) {
|
|
45
|
+
escaped += `\\x${codePoint.toString(16).padStart(2, "0")}`;
|
|
46
|
+
}
|
|
47
|
+
else if (codePoint >= 0x80 && codePoint <= 0x9f) {
|
|
48
|
+
escaped += `\\u${codePoint.toString(16).padStart(4, "0")}`;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
escaped += character;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return `"${escaped}"`;
|
|
55
|
+
}
|
|
28
56
|
const warning = (message) => { process.stderr.write(`session-journal: ${message}\n`); };
|
|
29
57
|
const journalDir = (root, date) => path.join(root, "workspace/threads/journal", date);
|
|
30
58
|
const hasFile = (file) => {
|
|
@@ -211,6 +239,7 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
|
|
|
211
239
|
return 0;
|
|
212
240
|
}
|
|
213
241
|
const slug = slugify(title) || "entry";
|
|
242
|
+
const yamlSlug = slug.includes("\n") ? yamlDoubleQuoted(slug) : slug;
|
|
214
243
|
const body = bodyFile && hasFile(bodyFile)
|
|
215
244
|
? fs.readFileSync(bodyFile, "utf8")
|
|
216
245
|
: "## Goal\n\n## Findings\n\n## Decisions\n\n## Next\n";
|
|
@@ -226,7 +255,7 @@ export function runSessionJournalFast(args, _stdin, cwd, hqRoot, dependencies =
|
|
|
226
255
|
for (let attempt = 0; attempt < 10_000; attempt += 1) {
|
|
227
256
|
seq = nextSequence(dir);
|
|
228
257
|
output = path.join(dir, `${seq}-${slug}.md`);
|
|
229
|
-
const lines = ["---\n", `ts: ${nowIso()}\n`, `title: ${title}\n`, `slug: ${
|
|
258
|
+
const lines = ["---\n", `ts: ${nowIso()}\n`, `title: ${yamlDoubleQuoted(title)}\n`, `slug: ${yamlSlug}\n`];
|
|
230
259
|
if (files) {
|
|
231
260
|
lines.push("files:\n");
|
|
232
261
|
for (const file of files.split(","))
|
|
@@ -12,6 +12,7 @@ import { DEFAULT_ORPHAN_USER_IDLE_HOURS, findOrphanLanes, parseOrphanMinAge, par
|
|
|
12
12
|
import { LANES_RUNS_REL } from "../lib/lanes/spawn.js";
|
|
13
13
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
14
14
|
import { checkCompanyPath } from "../lib/lanes/checked-company-path.js";
|
|
15
|
+
import { findOrphanedBareIdDropboxes } from "../lib/lanes/orphaned-dropbox.js";
|
|
15
16
|
export function mappingIncludesStory(mapping, storyId) {
|
|
16
17
|
return (mapping.storyPrefix === undefined || storyId.startsWith(mapping.storyPrefix))
|
|
17
18
|
&& (mapping.excludePrefix === undefined || !storyId.startsWith(mapping.excludePrefix));
|
|
@@ -810,6 +811,17 @@ async function checkReport(root, company, projects, directiveProjects, projectFi
|
|
|
810
811
|
host: finding.host,
|
|
811
812
|
})),
|
|
812
813
|
});
|
|
814
|
+
const orphanedBareIdDropboxes = findOrphanedBareIdDropboxes(root, company);
|
|
815
|
+
checks.push({
|
|
816
|
+
id: "orphaned_bare_id_dropbox",
|
|
817
|
+
result: orphanedBareIdDropboxes.length === 0 ? "ok" : "failing",
|
|
818
|
+
items: orphanedBareIdDropboxes.map((finding) => ({
|
|
819
|
+
id: finding.ulid,
|
|
820
|
+
reason: "orphaned_bare_id_dropbox",
|
|
821
|
+
message_count: finding.message_count,
|
|
822
|
+
newest_message_at: finding.newest_message_at,
|
|
823
|
+
})),
|
|
824
|
+
});
|
|
813
825
|
return {
|
|
814
826
|
ok: true,
|
|
815
827
|
company: company.slug,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { LaneCompanyRef, LaneRecord } from "../lib/lanes/types.js";
|
|
3
|
+
type WrapHandler = <Args extends unknown[]>(action: (...args: Args) => Promise<void>) => (...args: Args) => Promise<void>;
|
|
4
|
+
export interface LaneDropboxRepairDeps {
|
|
5
|
+
hqRoot: () => string;
|
|
6
|
+
now: () => Date;
|
|
7
|
+
resolveCompany: (company?: string) => Promise<LaneCompanyRef>;
|
|
8
|
+
repairEnabled: (lane: LaneRecord) => Promise<boolean>;
|
|
9
|
+
wrap: WrapHandler;
|
|
10
|
+
}
|
|
11
|
+
/** Register the explicit recovery command. Dry-run is read-only; mutation is separately gated. */
|
|
12
|
+
export declare function registerLaneDropboxRepairCommand(parent: Command, deps: LaneDropboxRepairDeps): void;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=lanes-dropbox-repair.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { LanesError } from "../lib/lanes/errors.js";
|
|
2
|
+
import { findOrphanedBareIdDropboxes, repairOrphanedBareIdDropbox } from "../lib/lanes/orphaned-dropbox.js";
|
|
3
|
+
import { readLane } from "../lib/lanes/store.js";
|
|
4
|
+
/** Register the explicit recovery command. Dry-run is read-only; mutation is separately gated. */
|
|
5
|
+
export function registerLaneDropboxRepairCommand(parent, deps) {
|
|
6
|
+
parent.command("repair-dropbox")
|
|
7
|
+
.description("Inspect or redeliver messages stranded in a bare-ULID lane dropbox")
|
|
8
|
+
.argument("<ulid>", "26-character lane ULID")
|
|
9
|
+
.option("--company <slug>", "Select the company by slug")
|
|
10
|
+
.option("--apply", "Append marked messages to the canonical lane inbox and archive the orphan folder")
|
|
11
|
+
.action(deps.wrap(async (ulid, options) => {
|
|
12
|
+
const root = deps.hqRoot();
|
|
13
|
+
const globalFinding = findOrphanedBareIdDropboxes(root).find((item) => item.ulid === ulid.trim().toUpperCase());
|
|
14
|
+
if (globalFinding?.canonical_lane_ids.length !== undefined && globalFinding.canonical_lane_ids.length > 1) {
|
|
15
|
+
throw new LanesError("orphaned_dropbox_lane_ambiguous", `ULID ${globalFinding.ulid} matches more than one lane record`, { ulid: globalFinding.ulid });
|
|
16
|
+
}
|
|
17
|
+
const company = await deps.resolveCompany(options.company);
|
|
18
|
+
const finding = findOrphanedBareIdDropboxes(root, company).find((item) => item.ulid === ulid.trim().toUpperCase());
|
|
19
|
+
if (!finding)
|
|
20
|
+
throw new LanesError("orphaned_dropbox_not_found", `No orphaned bare-ID dropbox was found for ${ulid}`, { ulid });
|
|
21
|
+
if (finding.canonical_lane_ids.length !== 1)
|
|
22
|
+
throw new LanesError("orphaned_dropbox_lane_ambiguous", `ULID ${finding.ulid} matches more than one lane record`, { ulid: finding.ulid });
|
|
23
|
+
const lane = readLane(root, finding.canonical_lane_ids[0]);
|
|
24
|
+
const enabled = options.apply ? await deps.repairEnabled(lane) : false;
|
|
25
|
+
const result = repairOrphanedBareIdDropbox(root, ulid, {
|
|
26
|
+
apply: options.apply === true,
|
|
27
|
+
enabled,
|
|
28
|
+
now: deps.now(),
|
|
29
|
+
});
|
|
30
|
+
console.log(JSON.stringify({ ok: true, action: "repair-dropbox", ...result }, null, 2));
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=lanes-dropbox-repair.js.map
|